Merge VS Code 1.26.1 (#2394)

* Squash merge commits for 1.26 (#1) (#2323)

* Polish tag search as per feedback (#55269)

* Polish tag search as per feedback

* Updated regex

* Allow users to opt-out of features that send online requests in the background (#55097)

* settings sweep #54690

* Minor css tweaks to enable eoverflow elipsis in more places (#55277)

* fix an issue with titlebarheight when not scaling with zoom

* Settings descriptions update #54690

* fixes #55209

* Settings editor - many padding fixes

* More space above level 2 label

* Fixing Cannot debug npm script using Yarn #55103

* Settings editor - show ellipsis when description overflows

* Settings editor - ... fix measuring around links, relayout

* Setting descriptions

* Settings editor - fix ... for some short lines, fix select container width

* Settings editor - overlay trees so scrollable shadow is full width

* Fix #54133 - missing extension settings after reload

* Settings color token description tweak

* Settings editor - disable overflow indicator temporarily, needs to be faster

* Added command to Run the selected npm script

* fixes #54452

* fixes #54929

* fixes #55248

* prefix command with extension name

* Contribute run selected to the context menu

* node-debug@1.26.6

* Allow terminal rendererType to be swapped out at runtime

Part of #53274
Fixes #55344

* Settings editor - fix not focusing search when restoring editor
setInput must be actually async. Will be fixed naturally when we aren't using winJS promises...

* Settings editor - TOC should only expand the section with a selected item

* Bump node-debug2

* Settings editor - Tree focus outlines

* Settings editor - don't blink the scrollbar when toc selection changes
And hide TOC correctly when the editor is narrow

* Settings editor - header rows should not be selectable

* fixes #54877

* change debug assignee to isi

* Settings sweep (#54690)

* workaround for #55051

* Settings sweep (#54690)

* settings sweep

#54690

* Don't try closing tags when you type > after another >

* Describe what implementation code lens does

Fixes #55370

* fix javadoc formatter setting description

* fixes #55325

* update to officical TS version

* Settings editor - Even more padding, use semibold instead of bold

* Fix #55357 - fix TOC twistie

* fixes #55288

* explorer: refresh on di change file system provider registration

fixes #53256

* Disable push to Linux repo to test standalone publisher

* New env var to notify log level to extensions #54001

* Disable snippets in extension search (when not in suggest dropdown) (#55281)

* Disable snippits in extension search (when not in suggest dropdown)

* Add monaco input contributions

* Fix bug preventing snippetSuggestions from taking effect in sub-editors

* Latest emmet helper to fix #52366

* Fix comment updates for threads within same file

* Allow extensions to log telemetry to log files #54001

* Pull latest css grammar

* files.exclude control - use same style for "add" vs "edit"

* files.exclude control - focus/keyboard behavior

* don't show menubar too early

* files.exclude - better styling

* Place cursor at end of extensions search box on autofill (#55254)

* Place cursor at end of extensions search box on autofill

* Use position instead of selection

* fix linux build issue (empty if block)

* Settings editor - fix extension category prefixes

* Settings editor - add simple ellipsis for first line that overflows, doesn't cover case when first line does not overflow but there is more text, TODO

* File/Text search provider docs

* Fixes #52655

* Include epoch (#55008)

* Fixes #53385

* Fixes #49480

*  VS Code Insiders (Users) not opening Fixes #55353

* Better handling of the case when the extension host fails to start

* Fixes #53966

*  Remove confusing Start from wordPartLeft commands ID

* vscode-xterm@3.6.0-beta12

Fixes #55488

* Initial size is set to infinity!! Fixes #55461

* Polish embeddedEditorBackground

* configuration service misses event

* Fix #55224 - fix duplicate results in multiroot workspace from splitting the diskseach query

* Select all not working in issue reporter on mac, fixes #55424

* Disable fuzzy matching for extensions autosuggest (#55498)

* Fix clipping of extensions search border in some third party themes (#55504)

* fixes #55538

* Fix bug causing an aria alert to not be shown the third time
 (and odd numbers thereafter)

* Settings editor - work around rendering glitch with webkit-line-clamp

* Settings editor - revert earlier '...' changes

* Settings editor - move enumDescription to its own div, because it disturbs -webkit-line-clamp for some reason

* Settings editor - better overflow indicator

* Don't show existing filters in autocomplete (#55495)

* Dont show existing filters in autocomplete

* Simplify

* Settings Editor: Add aria labels for input elements Fixes: #54836 (#55543)

* fixes #55223

* Update vscode-css-languageservice to 3.0.10-next.1

* Fix #55509 - settings navigation

* Fix #55519

* Fix #55520

* FIx #55524

* Fix #55556 - include wordSeparators in all search queries, so findTextInFiles can respect isWordMatch correctly

* oss updates for endgame

* Fix unit tests

* fixes #55522

* Avoid missing manifest error from bubbling up #54757

* Settings format crawl

* Search provider - Fix FileSearchProvider to return array, not progress

* Fix #55598

* Settings editor - fix NPE rendering settings with no description

* dont render inden guides in search box (#55600)

* fixes #55454

* More settings crawl

* Another change for #55598 - maxResults applies to FileSearch and TextSearch but not FileIndex

* Fix FileSearchProvider unit tests for progress change

* fixes #55561

* Settings description update for #54690

* Update setting descriptions for online services

* Minor edits

* fixes #55513

* fixes #55451

* Fix #55612 - fix findTextInFiles cancellation

* fixes #55539

* More setting description tweaks

* Setting to disable online experiments #54354

* fixes #55507

* fixes #55515

* Show online services action only in Insiders for now

* Settings editor - change toc behavior default to 'filter'

* Settings editor - nicer filter count style during search

* Fix #55617 - search viewlet icons

* Settings editor - better styling for element count indicator

* SearchProvider - fix NPE when searching extraFileResources

* Allow extends to work without json suffix

Fixes #16905

* Remove accessability options logic entirely

Follow up on #55451

* use latest version of DAP

* fixes #55490

* fixes #55122

* fixes #52332

* Avoid assumptions about git: URIs (fixes #36236)

* relative path for descriptions

* resourece: get rid of isFile context key

fixes #48275

* Register previous ids for compatibility (#53497)

* more tuning for #48275

* no need to always re-read "files explorer"

fixes #52003

* read out active composites properly

fixes #51967

* Update link colors for hc theme to meet color contrast ratio, fixes #55651

Also updated link color for `textLinkActiveForeground` to be the same as `textLinkForeground` as it wasn't properly updated

* detect 'winpty-agent.exe'; fixes #55672

* node-debug@1.26.7

* reset counter on new label

* Settings editor - fix multiple setting links in one description

* Settings editor - color code blocks in setting descriptions, fix #55532

* Settings editor - hover color in TOC

* Settings editor - fix navigation NPE

* Settings editor - fix text control width

* Settings editor - maybe fix #55684

* Fix bug causing cursor to not move on paste

* fixes #53582

* Use ctrlCmd instead of ctrl for go down from search box

* fixes #55264

* fixes #55456

* filter for spcaes before triggering search (#55611)

* Fix #55698 - don't lose filtered TOC counts when refreshing TOC

* fixes #55421

* fixes #28979

* fixes #55576

* only add check for updates to windows/linux help

* readonly files: append decoration to label

fixes #53022

* debug: do not show toolbar while initialising

fixes #55026

* Opening launch.json should not activate debug extensions

fixes #55029

* fixes #55435

* fixes #55434

* fixes #55439

* trigger menu only on altkey up

* Fix #50555 - fix settings editor memory leak

* Fix #55712 - no need to focus 'a' anymore when restoring control focus after tree render

* fixes #55335

* proper fix for readonly model

fixes #53022

* improve FoldingRangeKind spec (for #55686)

* Use class with static fields (fixes #55494)

* Fixes #53671

* fixes #54630

* [html] should disable ionic suggestions by default. Currently forces deprecated Ionic v1 suggestions in .html files while typing. Fixes #53324

* cleanup deps

* debug issues back to andre

* update electron for smoketest

* Fix #55757 - prevent settings tabs from overflowing

* Fix #53897 - revert setting menu defaults to old editor

* Add enum descriptions to `typescript.preferences.importModuleSpecifier`

* Fix #55767 - leaking style elements from settings editor

* Fix #55521 - prevent flashing when clicking in exclude control

* Update Git modified color for contrast ratio, fixes #53140

* Revert "Merge branch 'master' of github.com:Microsoft/vscode"

This reverts commit bf46b6bfbae0cab99c2863e1244a916181fa9fbc, reversing
changes made to e275a424483dfb4ed33b428c97d5e2c441d6b917.

* Revert "Revert "Merge branch 'master' of github.com:Microsoft/vscode""

This reverts commit 53949d963f39e40757557c6526332354a31d9154.

* don't ask to install an incomplete menu

* Fix NPE in terminal AccessibilityManager

Fixes #55744

* don't display fallback menu unless we've closed the last window

* fixes #55547

* Fix smoke tests for extension search box

* Update OSSREADME.json for Electron 2.0.5

* Update distro

Includes Chromium license changes

* fix #55455

* fix #55865

* fixes #55893

* Fix bug causing workspace recommendations to go away upon ignoring a recommendation (#55805)

* Fix bug causing workspace recommendations to go away upon ignoring a recommendation

* ONly show on @recommended or @recommended:workspace

* Make more consistant

* Fix #55911

* Understand json activity (#55926)

* Understand json file activity

* Refactoring

* adding composer.json

* Distro update for experiments

* use terminal.processId for auto-attach; fixes #55918

* Reject invalid URI with vscode.openFolder (for #55891)

* improve win32 setup system vs user detection

fixes #55840

fixes #55840

delay winreg import

related to #55840

show notification earlier

related to #55840

fix #55840

update inno setup message

related to #55840

* Fix #55593 - this code only operates on local paths, so use fsPath and Uri.file instead

* Bring back the old menu due to electron 2.0 issues (#55913)

* add the old menu back for native menus

* make menu labels match

* `vscode.openFolder`: treat missing URI schema gracefully (for #55891)

* delay EH reattach; fixes #55955

* Mark all json files under appSettingsHome as settings

* Use localized strings for telemetry opt-out

* Exception when saving file editor opened from remote file provider (fixes #55051)

* Remove terminal menu from stable

Fixes 56003

* VSCode Insiders crashes on open with TypeError: Cannot read property 'lastIndexOf' of undefined. Fixes #54933

* improve fix for #55891

* fix #55916

* Improve #55891

* increase EH debugging restart delay; fixes #55955

* Revert "Don't include non-resource entries in history quick pick"

This reverts commit 37209a838e9f7e9abe6dc53ed73cdf1e03b72060.

* Diff editor: horizontal scrollbar height is smaller (fixes #56062)

* improve openFolder uri fix (correctly treat backslashes)

* fixes #56116
repair ipc for native menubar keybindings

* Fix #56240 - Open the JSON settings editor instead of the UI editor

* Fix #55536

* uriDisplay: if no formatter is registered fall back to getPathlabel

fixes #56104

* VSCode hangs when opening python file. Fixes #56377

* VS Code Hangs When Opening Specific PowerShell File. Fixes #56430

* Fix #56433 - search extraFileResources even when no folders open

* Workaround #55649

* Fix in master #56371

* Fix tests #56371

* Fix in master #56317

* increase version to 1.26.1

* Fixes #56387: Handle SIGPIPE in extension host

* fixes #56185

* Fix merge issues (part 1)

* Fix build breaks (part 1)

* Build breaks (part 2)

* Build breaks (part 3)

* More build breaks (part 4)

* Fix build breaks (part 5)

* WIP

* Fix menus

* Render query result and message panels (#2363)

* Put back query editor hot exit changes

* Fix grid changes that broke profiler (#2365)

* Update APIs for saving query editor state

* Fix restore view state for profiler and edit data

* Updating custom default themes to support 4.5:1 contrast ratio

* Test updates

* Fix Extension Manager and Windows Setup

* Update license headers

* Add appveyor and travis files back

* Fix hidden modal dropdown issue
This commit is contained in:
Karl Burtram
2018-09-04 14:55:00 -07:00
committed by GitHub
parent 3763278366
commit 81329fa7fa
2638 changed files with 118456 additions and 64012 deletions

View File

@@ -6,12 +6,13 @@
import { localize } from 'vs/nls';
import { isFalsyOrWhitespace } from 'vs/base/common/strings';
import { join } from 'path';
import * as resources from 'vs/base/common/resources';
import { IJSONSchema } from 'vs/base/common/jsonSchema';
import { forEach } from 'vs/base/common/collections';
import { IExtensionPointUser, ExtensionMessageCollector, ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry';
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { MenuId, MenuRegistry, ILocalizedString } from 'vs/platform/actions/common/actions';
import URI from 'vs/base/common/uri';
namespace schema {
@@ -288,19 +289,19 @@ ExtensionsRegistry.registerExtensionPoint<schema.IUserFriendlyCommand | schema.I
const { icon, category, title, command } = userFriendlyCommand;
let absoluteIcon: { dark: string; light?: string; };
let absoluteIcon: { dark: URI; light?: URI; };
if (icon) {
if (typeof icon === 'string') {
absoluteIcon = { dark: join(extension.description.extensionFolderPath, icon) };
absoluteIcon = { dark: resources.joinPath(extension.description.extensionLocation, icon) };
} else {
absoluteIcon = {
dark: join(extension.description.extensionFolderPath, icon.dark),
light: join(extension.description.extensionFolderPath, icon.light)
dark: resources.joinPath(extension.description.extensionLocation, icon.dark),
light: resources.joinPath(extension.description.extensionLocation, icon.light)
};
}
}
if (MenuRegistry.addCommand({ id: command, title, category, iconPath: absoluteIcon })) {
if (MenuRegistry.addCommand({ id: command, title, category, iconLocation: absoluteIcon })) {
extension.collector.info(localize('dup', "Command `{0}` appears multiple times in the `commands` section.", userFriendlyCommand.command));
}
}

View File

@@ -5,71 +5,16 @@
'use strict';
import * as assert from 'assert';
import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions';
import { MenuRegistry, MenuId, isIMenuItem } from 'vs/platform/actions/common/actions';
import { MenuService } from 'vs/workbench/services/actions/common/menuService';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { NullCommandService } from 'vs/platform/commands/common/commands';
import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService';
import { IExtensionPoint } from 'vs/workbench/services/extensions/common/extensionsRegistry';
import { TPromise } from 'vs/base/common/winjs.base';
import { ExtensionPointContribution, IExtensionDescription, IExtensionsStatus, IExtensionService, ProfileSession } from 'vs/workbench/services/extensions/common/extensions';
import { Event, Emitter } from 'vs/base/common/event';
import { TestExtensionService } from 'vs/workbench/test/workbenchTestServices';
// --- service instances
class MockExtensionService implements IExtensionService {
public _serviceBrand: any;
private _onDidRegisterExtensions = new Emitter<void>();
public get onDidRegisterExtensions(): Event<void> {
return this._onDidRegisterExtensions.event;
}
onDidChangeExtensionsStatus = null;
public activateByEvent(activationEvent: string): TPromise<void> {
throw new Error('Not implemented');
}
public whenInstalledExtensionsRegistered(): TPromise<boolean> {
return TPromise.as(true);
}
public getExtensions(): TPromise<IExtensionDescription[]> {
throw new Error('Not implemented');
}
public readExtensionPointContributions<T>(extPoint: IExtensionPoint<T>): TPromise<ExtensionPointContribution<T>[]> {
throw new Error('Not implemented');
}
public getExtensionsStatus(): { [id: string]: IExtensionsStatus; } {
throw new Error('Not implemented');
}
public canProfileExtensionHost() {
return false;
}
public startExtensionHostProfile(): TPromise<ProfileSession> {
throw new Error('Not implemented');
}
public restartExtensionHost(): void {
throw new Error('Method not implemented.');
}
public startExtensionHost(): void {
throw new Error('Method not implemented.');
}
public stopExtensionHost(): void {
throw new Error('Method not implemented.');
}
}
const extensionService = new MockExtensionService();
const extensionService = new TestExtensionService();
const contextKeyService = new class extends MockContextKeyService {
contextMatchesRules() {
@@ -245,13 +190,15 @@ suite('MenuService', function () {
let foundA = false;
let foundB = false;
for (const item of MenuRegistry.getMenuItems(MenuId.CommandPalette)) {
if (item.command.id === 'a') {
assert.equal(item.command.title, 'Explicit');
foundA = true;
}
if (item.command.id === 'b') {
assert.equal(item.command.title, 'Implicit');
foundB = true;
if (isIMenuItem(item)) {
if (item.command.id === 'a') {
assert.equal(item.command.title, 'Explicit');
foundA = true;
}
if (item.command.id === 'b') {
assert.equal(item.command.title, 'Implicit');
foundB = true;
}
}
}
assert.equal(foundA, true);

View File

@@ -21,7 +21,7 @@ export class ActivityService implements IActivityService {
@IPanelService private panelService: IPanelService
) { }
public showActivity(compositeOrActionId: string, badge: IBadge, clazz?: string, priority?: number): IDisposable {
showActivity(compositeOrActionId: string, badge: IBadge, clazz?: string, priority?: number): IDisposable {
if (this.panelService.getPanels().filter(p => p.id === compositeOrActionId).length) {
return this.panelPart.showActivity(compositeOrActionId, badge, clazz);
}

View File

@@ -13,19 +13,19 @@ export interface IBadge {
}
export class BaseBadge implements IBadge {
public descriptorFn: (args: any) => string;
descriptorFn: (args: any) => string;
constructor(descriptorFn: (args: any) => string) {
this.descriptorFn = descriptorFn;
}
public getDescription(): string {
getDescription(): string {
return this.descriptorFn(null);
}
}
export class NumberBadge extends BaseBadge {
public number: number;
number: number;
constructor(number: number, descriptorFn: (args: any) => string) {
super(descriptorFn);
@@ -33,13 +33,13 @@ export class NumberBadge extends BaseBadge {
this.number = number;
}
public getDescription(): string {
getDescription(): string {
return this.descriptorFn(this.number);
}
}
export class TextBadge extends BaseBadge {
public text: string;
text: string;
constructor(text: string, descriptorFn: (args: any) => string) {
super(descriptorFn);

View File

@@ -22,11 +22,6 @@ export const BACKUP_FILE_UPDATE_OPTIONS: IUpdateContentOptions = { encoding: 'ut
export interface IBackupFileService {
_serviceBrand: any;
/**
* If backups are enabled.
*/
backupEnabled: boolean;
/**
* Finds out if there are any backups stored.
*/

View File

@@ -15,7 +15,8 @@ import { IFileService, ITextSnapshot } from 'vs/platform/files/common/files';
import { TPromise } from 'vs/base/common/winjs.base';
import { readToMatchingString } from 'vs/base/node/stream';
import { ITextBufferFactory } from 'vs/editor/common/model';
import { createTextBufferFactoryFromStream } from 'vs/editor/common/model/textModel';
import { createTextBufferFactoryFromStream, createTextBufferFactoryFromSnapshot } from 'vs/editor/common/model/textModel';
import { keys } from 'vs/base/common/map';
export interface IBackupFilesModel {
resolve(backupRoot: string): TPromise<IBackupFilesModel>;
@@ -31,10 +32,9 @@ export interface IBackupFilesModel {
export class BackupSnapshot implements ITextSnapshot {
private preambleHandled: boolean;
constructor(private snapshot: ITextSnapshot, private preamble: string) {
}
constructor(private snapshot: ITextSnapshot, private preamble: string) { }
public read(): string {
read(): string {
let value = this.snapshot.read();
if (!this.preambleHandled) {
this.preambleHandled = true;
@@ -53,7 +53,7 @@ export class BackupSnapshot implements ITextSnapshot {
export class BackupFilesModel implements IBackupFilesModel {
private cache: { [resource: string]: number /* version ID */ } = Object.create(null);
public resolve(backupRoot: string): TPromise<IBackupFilesModel> {
resolve(backupRoot: string): TPromise<IBackupFilesModel> {
return pfs.readDirsInDir(backupRoot).then(backupSchemas => {
// For all supported schemas
@@ -73,15 +73,15 @@ export class BackupFilesModel implements IBackupFilesModel {
}).then(() => this, error => this);
}
public add(resource: Uri, versionId = 0): void {
add(resource: Uri, versionId = 0): void {
this.cache[resource.toString()] = versionId;
}
public count(): number {
count(): number {
return Object.keys(this.cache).length;
}
public has(resource: Uri, versionId?: number): boolean {
has(resource: Uri, versionId?: number): boolean {
const cachedVersionId = this.cache[resource.toString()];
if (typeof cachedVersionId !== 'number') {
return false; // unknown resource
@@ -94,15 +94,15 @@ export class BackupFilesModel implements IBackupFilesModel {
return true;
}
public get(): Uri[] {
get(): Uri[] {
return Object.keys(this.cache).map(k => Uri.parse(k));
}
public remove(resource: Uri): void {
remove(resource: Uri): void {
delete this.cache[resource.toString()];
}
public clear(): void {
clear(): void {
this.cache = Object.create(null);
}
}
@@ -111,7 +111,7 @@ export class BackupFileService implements IBackupFileService {
private static readonly META_MARKER = '\n';
public _serviceBrand: any;
_serviceBrand: any;
private backupWorkspacePath: string;
@@ -129,40 +129,29 @@ export class BackupFileService implements IBackupFileService {
this.initialize(backupWorkspacePath);
}
public initialize(backupWorkspacePath: string): void {
initialize(backupWorkspacePath: string): void {
this.backupWorkspacePath = backupWorkspacePath;
this.ready = this.init();
}
public get backupEnabled(): boolean {
return !!this.backupWorkspacePath; // Hot exit requires a backup path
}
private init(): TPromise<IBackupFilesModel> {
const model = new BackupFilesModel();
if (!this.backupEnabled) {
return TPromise.as(model);
}
return model.resolve(this.backupWorkspacePath);
}
public hasBackups(): TPromise<boolean> {
hasBackups(): TPromise<boolean> {
return this.ready.then(model => {
return model.count() > 0;
});
}
public loadBackupResource(resource: Uri): TPromise<Uri> {
loadBackupResource(resource: Uri): TPromise<Uri> {
return this.ready.then(model => {
const backupResource = this.toBackupResource(resource);
if (!backupResource) {
return void 0;
}
// Return directly if we have a known backup with that resource
const backupResource = this.toBackupResource(resource);
if (model.has(backupResource)) {
return backupResource;
}
@@ -171,17 +160,13 @@ export class BackupFileService implements IBackupFileService {
});
}
public backupResource(resource: Uri, content: ITextSnapshot, versionId?: number): TPromise<void> {
backupResource(resource: Uri, content: ITextSnapshot, versionId?: number): TPromise<void> {
if (this.isShuttingDown) {
return TPromise.as(void 0);
}
return this.ready.then(model => {
const backupResource = this.toBackupResource(resource);
if (!backupResource) {
return void 0;
}
if (model.has(backupResource, versionId)) {
return void 0; // return early if backup version id matches requested one
}
@@ -195,12 +180,9 @@ export class BackupFileService implements IBackupFileService {
});
}
public discardResourceBackup(resource: Uri): TPromise<void> {
discardResourceBackup(resource: Uri): TPromise<void> {
return this.ready.then(model => {
const backupResource = this.toBackupResource(resource);
if (!backupResource) {
return void 0;
}
return this.ioOperationQueues.queueFor(backupResource).queue(() => {
return pfs.del(backupResource.fsPath).then(() => model.remove(backupResource));
@@ -208,26 +190,21 @@ export class BackupFileService implements IBackupFileService {
});
}
public discardAllWorkspaceBackups(): TPromise<void> {
discardAllWorkspaceBackups(): TPromise<void> {
this.isShuttingDown = true;
return this.ready.then(model => {
if (!this.backupEnabled) {
return void 0;
}
return pfs.del(this.backupWorkspacePath).then(() => model.clear());
});
}
public getWorkspaceFileBackups(): TPromise<Uri[]> {
getWorkspaceFileBackups(): TPromise<Uri[]> {
return this.ready.then(model => {
const readPromises: TPromise<Uri>[] = [];
model.get().forEach(fileBackup => {
readPromises.push(
readToMatchingString(fileBackup.fsPath, BackupFileService.META_MARKER, 2000, 10000)
.then(Uri.parse)
readToMatchingString(fileBackup.fsPath, BackupFileService.META_MARKER, 2000, 10000).then(Uri.parse)
);
});
@@ -235,7 +212,7 @@ export class BackupFileService implements IBackupFileService {
});
}
public resolveBackupContent(backup: Uri): TPromise<ITextBufferFactory> {
resolveBackupContent(backup: Uri): TPromise<ITextBufferFactory> {
return this.fileService.resolveStreamContent(backup, BACKUP_FILE_RESOLVE_OPTIONS).then(content => {
// Add a filter method to filter out everything until the meta marker
@@ -258,11 +235,7 @@ export class BackupFileService implements IBackupFileService {
});
}
public toBackupResource(resource: Uri): Uri {
if (!this.backupEnabled) {
return null;
}
toBackupResource(resource: Uri): Uri {
return Uri.file(path.join(this.backupWorkspacePath, resource.scheme, this.hashPath(resource)));
}
@@ -270,3 +243,63 @@ export class BackupFileService implements IBackupFileService {
return crypto.createHash('md5').update(resource.fsPath).digest('hex');
}
}
export class InMemoryBackupFileService implements IBackupFileService {
_serviceBrand: any;
private backups: Map<string, ITextSnapshot> = new Map();
hasBackups(): TPromise<boolean> {
return TPromise.as(this.backups.size > 0);
}
loadBackupResource(resource: Uri): TPromise<Uri> {
const backupResource = this.toBackupResource(resource);
if (this.backups.has(backupResource.toString())) {
return TPromise.as(backupResource);
}
return TPromise.as(void 0);
}
backupResource(resource: Uri, content: ITextSnapshot, versionId?: number): TPromise<void> {
const backupResource = this.toBackupResource(resource);
this.backups.set(backupResource.toString(), content);
return TPromise.as(void 0);
}
resolveBackupContent(backupResource: Uri): TPromise<ITextBufferFactory> {
const snapshot = this.backups.get(backupResource.toString());
if (snapshot) {
return TPromise.as(createTextBufferFactoryFromSnapshot(snapshot));
}
return TPromise.as(void 0);
}
getWorkspaceFileBackups(): TPromise<Uri[]> {
return TPromise.as(keys(this.backups).map(key => Uri.parse(key)));
}
discardResourceBackup(resource: Uri): TPromise<void> {
this.backups.delete(this.toBackupResource(resource).toString());
return TPromise.as(void 0);
}
discardAllWorkspaceBackups(): TPromise<void> {
this.backups.clear();
return TPromise.as(void 0);
}
toBackupResource(resource: Uri): Uri {
return Uri.file(path.join(resource.scheme, this.hashPath(resource)));
}
private hashPath(resource: Uri): string {
return crypto.createHash('md5').update(resource.fsPath).digest('hex');
}
}

View File

@@ -0,0 +1,418 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { mergeSort } from 'vs/base/common/arrays';
import { dispose, IDisposable, IReference } from 'vs/base/common/lifecycle';
import URI from 'vs/base/common/uri';
import { TPromise } from 'vs/base/common/winjs.base';
import { ICodeEditor, isCodeEditor } from 'vs/editor/browser/editorBrowser';
import { IBulkEditOptions, IBulkEditResult, IBulkEditService } from 'vs/editor/browser/services/bulkEditService';
import { EditOperation } from 'vs/editor/common/core/editOperation';
import { Range } from 'vs/editor/common/core/range';
import { EndOfLineSequence, IIdentifiedSingleEditOperation, ITextModel } from 'vs/editor/common/model';
import { isResourceFileEdit, isResourceTextEdit, ResourceFileEdit, ResourceTextEdit, WorkspaceEdit } from 'vs/editor/common/modes';
import { IModelService } from 'vs/editor/common/services/modelService';
import { ITextEditorModel, ITextModelService } from 'vs/editor/common/services/resolverService';
import { localize } from 'vs/nls';
import { IFileService } 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 { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
import { IUriDisplayService } from 'vs/platform/uriDisplay/common/uriDisplay';
abstract class Recording {
static start(fileService: IFileService): Recording {
let _changes = new Set<string>();
let subscription = fileService.onAfterOperation(e => {
_changes.add(e.resource.toString());
});
return {
stop() { return subscription.dispose(); },
hasChanged(resource) { return _changes.has(resource.toString()); }
};
}
abstract stop(): void;
abstract hasChanged(resource: URI): boolean;
}
type ValidationResult = { canApply: true } | { canApply: false, reason: URI };
class ModelEditTask implements IDisposable {
private readonly _model: ITextModel;
protected _edits: IIdentifiedSingleEditOperation[];
private _expectedModelVersionId: number | undefined;
protected _newEol: EndOfLineSequence;
constructor(private readonly _modelReference: IReference<ITextEditorModel>) {
this._model = this._modelReference.object.textEditorModel;
this._edits = [];
}
dispose() {
dispose(this._modelReference);
}
addEdit(resourceEdit: ResourceTextEdit): void {
this._expectedModelVersionId = resourceEdit.modelVersionId;
for (const edit of resourceEdit.edits) {
if (typeof edit.eol === 'number') {
// honor eol-change
this._newEol = edit.eol;
}
if (edit.range || edit.text) {
// create edit operation
let range: Range;
if (!edit.range) {
range = this._model.getFullModelRange();
} else {
range = Range.lift(edit.range);
}
this._edits.push(EditOperation.replaceMove(range, edit.text));
}
}
}
validate(): ValidationResult {
if (typeof this._expectedModelVersionId === 'undefined' || this._model.getVersionId() === this._expectedModelVersionId) {
return { canApply: true };
}
return { canApply: false, reason: this._model.uri };
}
apply(): void {
if (this._edits.length > 0) {
this._edits = mergeSort(this._edits, (a, b) => Range.compareRangesUsingStarts(a.range, b.range));
this._model.pushStackElement();
this._model.pushEditOperations([], this._edits, () => []);
this._model.pushStackElement();
}
if (this._newEol !== undefined) {
this._model.pushStackElement();
this._model.pushEOL(this._newEol);
this._model.pushStackElement();
}
}
}
class EditorEditTask extends ModelEditTask {
private _editor: ICodeEditor;
constructor(modelReference: IReference<ITextEditorModel>, editor: ICodeEditor) {
super(modelReference);
this._editor = editor;
}
apply(): void {
if (this._edits.length > 0) {
this._edits = mergeSort(this._edits, (a, b) => Range.compareRangesUsingStarts(a.range, b.range));
this._editor.pushUndoStop();
this._editor.executeEdits('', this._edits);
this._editor.pushUndoStop();
}
if (this._newEol !== undefined) {
this._editor.pushUndoStop();
this._editor.getModel().pushEOL(this._newEol);
this._editor.pushUndoStop();
}
}
}
class BulkEditModel implements IDisposable {
private _textModelResolverService: ITextModelService;
private _edits = new Map<string, ResourceTextEdit[]>();
private _editor: ICodeEditor;
private _tasks: ModelEditTask[];
private _progress: IProgress<void>;
constructor(
textModelResolverService: ITextModelService,
editor: ICodeEditor,
edits: ResourceTextEdit[],
progress: IProgress<void>
) {
this._textModelResolverService = textModelResolverService;
this._editor = editor;
this._progress = progress;
edits.forEach(this.addEdit, this);
}
dispose(): void {
this._tasks = dispose(this._tasks);
}
addEdit(edit: ResourceTextEdit): void {
let array = this._edits.get(edit.resource.toString());
if (!array) {
array = [];
this._edits.set(edit.resource.toString(), array);
}
array.push(edit);
}
async prepare(): Promise<BulkEditModel> {
if (this._tasks) {
throw new Error('illegal state - already prepared');
}
this._tasks = [];
const promises: TPromise<any>[] = [];
this._edits.forEach((value, key) => {
const promise = this._textModelResolverService.createModelReference(URI.parse(key)).then(ref => {
const model = ref.object;
if (!model || !model.textEditorModel) {
throw new Error(`Cannot load file ${key}`);
}
let task: ModelEditTask;
if (this._editor && this._editor.getModel().uri.toString() === model.textEditorModel.uri.toString()) {
task = new EditorEditTask(ref, this._editor);
} else {
task = new ModelEditTask(ref);
}
value.forEach(edit => task.addEdit(edit));
this._tasks.push(task);
this._progress.report(undefined);
});
promises.push(promise);
});
await TPromise.join(promises);
return this;
}
validate(): ValidationResult {
for (const task of this._tasks) {
const result = task.validate();
if (!result.canApply) {
return result;
}
}
return { canApply: true };
}
apply(): void {
for (const task of this._tasks) {
task.apply();
this._progress.report(undefined);
}
}
}
export type Edit = ResourceFileEdit | ResourceTextEdit;
export class BulkEdit {
private _edits: Edit[] = [];
private _editor: ICodeEditor;
private _progress: IProgressRunner;
constructor(
editor: ICodeEditor,
progress: IProgressRunner,
@ILogService private readonly _logService: ILogService,
@ITextModelService private readonly _textModelService: ITextModelService,
@IFileService private readonly _fileService: IFileService,
@ITextFileService private readonly _textFileService: ITextFileService,
@IUriDisplayService private readonly _uriDisplayServie: IUriDisplayService
) {
this._editor = editor;
this._progress = progress || emptyProgressRunner;
}
add(edits: Edit[] | Edit): void {
if (Array.isArray(edits)) {
this._edits.push(...edits);
} else {
this._edits.push(edits);
}
}
ariaMessage(): string {
const editCount = this._edits.reduce((prev, cur) => isResourceFileEdit(cur) ? prev : prev + cur.edits.length, 0);
const resourceCount = this._edits.length;
if (editCount === 0) {
return localize('summary.0', "Made no edits");
} else if (editCount > 1 && resourceCount > 1) {
return localize('summary.nm', "Made {0} text edits in {1} files", editCount, resourceCount);
} else {
return localize('summary.n0', "Made {0} text edits in one file", editCount, resourceCount);
}
}
async perform(): Promise<void> {
let seen = new Set<string>();
let total = 0;
const groups: Edit[][] = [];
let group: Edit[];
for (const edit of this._edits) {
if (!group
|| (isResourceFileEdit(group[0]) && !isResourceFileEdit(edit))
|| (isResourceTextEdit(group[0]) && !isResourceTextEdit(edit))
) {
group = [];
groups.push(group);
}
group.push(edit);
if (isResourceFileEdit(edit)) {
total += 1;
} else if (!seen.has(edit.resource.toString())) {
seen.add(edit.resource.toString());
total += 2;
}
}
// define total work and progress callback
// for child operations
this._progress.total(total);
let progress: IProgress<void> = { report: _ => this._progress.worked(1) };
// do it.
for (const group of groups) {
if (isResourceFileEdit(group[0])) {
await this._performFileEdits(<ResourceFileEdit[]>group, progress);
} else {
await this._performTextEdits(<ResourceTextEdit[]>group, progress);
}
}
}
private async _performFileEdits(edits: ResourceFileEdit[], progress: IProgress<void>) {
this._logService.debug('_performFileEdits', JSON.stringify(edits));
for (const edit of edits) {
progress.report(undefined);
let options = edit.options || {};
if (edit.newUri && edit.oldUri) {
// rename
if (options.overwrite === undefined && options.ignoreIfExists && await this._fileService.existsFile(edit.newUri)) {
continue; // not overwriting, but ignoring, and the target file exists
}
await this._textFileService.move(edit.oldUri, edit.newUri, options.overwrite);
} else if (!edit.newUri && edit.oldUri) {
// delete file
if (!options.ignoreIfNotExists || await this._fileService.existsFile(edit.oldUri)) {
await this._textFileService.delete(edit.oldUri, { useTrash: true, recursive: options.recursive });
}
} else if (edit.newUri && !edit.oldUri) {
// create file
if (options.overwrite === undefined && options.ignoreIfExists && await this._fileService.existsFile(edit.newUri)) {
continue; // not overwriting, but ignoring, and the target file exists
}
await this._textFileService.create(edit.newUri, undefined, { overwrite: options.overwrite });
}
}
}
private async _performTextEdits(edits: ResourceTextEdit[], progress: IProgress<void>): Promise<void> {
this._logService.debug('_performTextEdits', JSON.stringify(edits));
const recording = Recording.start(this._fileService);
const model = new BulkEditModel(this._textModelService, this._editor, edits, progress);
await model.prepare();
const conflicts = edits
.filter(edit => recording.hasChanged(edit.resource))
.map(edit => this._uriDisplayServie.getLabel(edit.resource, true));
recording.stop();
if (conflicts.length > 0) {
model.dispose();
throw new Error(localize('conflict', "These files have changed in the meantime: {0}", conflicts.join(', ')));
}
const validationResult = model.validate();
if (validationResult.canApply === false) {
throw new Error(`${validationResult.reason.toString()} has changed in the meantime`);
}
await model.apply();
model.dispose();
}
}
export class BulkEditService implements IBulkEditService {
_serviceBrand: any;
constructor(
@ILogService private readonly _logService: ILogService,
@IModelService private readonly _modelService: IModelService,
@IEditorService private readonly _editorService: IEditorService,
@ITextModelService private readonly _textModelService: ITextModelService,
@IFileService private readonly _fileService: IFileService,
@ITextFileService private readonly _textFileService: ITextFileService,
@IUriDisplayService private readonly _uriDisplayService: IUriDisplayService
) {
}
apply(edit: WorkspaceEdit, options: IBulkEditOptions = {}): TPromise<IBulkEditResult> {
let { edits } = edit;
let codeEditor = options.editor;
// First check if loaded models were not changed in the meantime
for (let i = 0, len = edits.length; i < len; i++) {
const edit = edits[i];
if (!isResourceFileEdit(edit) && typeof edit.modelVersionId === 'number') {
let model = this._modelService.getModel(edit.resource);
if (model && model.getVersionId() !== edit.modelVersionId) {
// model changed in the meantime
return TPromise.wrapError(new Error(`${model.uri.toString()} has changed in the meantime`));
}
}
}
// try to find code editor
// todo@joh, prefer edit that gets edited
if (!codeEditor) {
let candidate = this._editorService.activeTextEditorWidget;
if (isCodeEditor(candidate)) {
codeEditor = candidate;
}
}
const bulkEdit = new BulkEdit(options.editor, options.progress, this._logService, this._textModelService, this._fileService, this._textFileService, this._uriDisplayService);
bulkEdit.add(edits);
return TPromise.wrap(bulkEdit.perform().then(() => {
return { ariaSummary: bulkEdit.ariaMessage() };
}, err => {
// console.log('apply FAILED');
// console.log(err);
this._logService.error(err);
throw err;
}));
}
}
registerSingleton(IBulkEditService, BulkEditService);

View File

@@ -0,0 +1,78 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { CodeEditorServiceImpl } from 'vs/editor/browser/services/codeEditorServiceImpl';
import { ICodeEditor, isCodeEditor, isDiffEditor } from 'vs/editor/browser/editorBrowser';
import { IResourceInput } from 'vs/platform/editor/common/editor';
import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { TPromise } from 'vs/base/common/winjs.base';
import { TextEditorOptions } from 'vs/workbench/common/editor';
import { ScrollType } from 'vs/editor/common/editorCommon';
export class CodeEditorService extends CodeEditorServiceImpl {
constructor(
@IEditorService private editorService: IEditorService,
@IThemeService themeService: IThemeService
) {
super(themeService);
}
getActiveCodeEditor(): ICodeEditor {
const activeTextEditorWidget = this.editorService.activeTextEditorWidget;
if (isCodeEditor(activeTextEditorWidget)) {
return activeTextEditorWidget;
}
if (isDiffEditor(activeTextEditorWidget)) {
return activeTextEditorWidget.getModifiedEditor();
}
return null;
}
openCodeEditor(input: IResourceInput, source: ICodeEditor, sideBySide?: boolean): TPromise<ICodeEditor> {
// Special case: If the active editor is a diff editor and the request to open originates and
// targets the modified side of it, we just apply the request there to prevent opening the modified
// side as separate editor.
const activeTextEditorWidget = this.editorService.activeTextEditorWidget;
if (
!sideBySide && // we need the current active group to be the taret
isDiffEditor(activeTextEditorWidget) && // we only support this for active text diff editors
input.options && // we need options to apply
input.resource && // we need a request resource to compare with
activeTextEditorWidget.getModel() && // we need a target model to compare with
source === activeTextEditorWidget.getModifiedEditor() && // we need the source of this request to be the modified side of the diff editor
input.resource.toString() === activeTextEditorWidget.getModel().modified.uri.toString() // we need the input resources to match with modified side
) {
const targetEditor = activeTextEditorWidget.getModifiedEditor();
const textOptions = TextEditorOptions.create(input.options);
textOptions.apply(targetEditor, ScrollType.Smooth);
return TPromise.as(targetEditor);
}
// Open using our normal editor service
return this.doOpenCodeEditor(input, source, sideBySide);
}
private doOpenCodeEditor(input: IResourceInput, source: ICodeEditor, sideBySide?: boolean): TPromise<ICodeEditor> {
return this.editorService.openEditor(input, sideBySide ? SIDE_GROUP : ACTIVE_GROUP).then(control => {
if (control) {
const widget = control.getControl();
if (isCodeEditor(widget)) {
return widget;
}
}
return null;
});
}
}

View File

@@ -11,6 +11,7 @@ import { ExtensionsRegistry, IExtensionPointUser } from 'vs/workbench/services/e
import { IConfigurationNode, IConfigurationRegistry, Extensions, editorConfigurationSchemaId, IDefaultConfigurationExtension, validateProperty, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry';
import { IJSONContributionRegistry, Extensions as JSONExtensions } from 'vs/platform/jsonschemas/common/jsonContributionRegistry';
import { workspaceSettingsSchemaId, launchSchemaId } from 'vs/workbench/services/configuration/common/configuration';
import { isObject } from 'vs/base/common/types';
const configurationRegistry = Registry.as<IConfigurationRegistry>(Extensions.Configuration);
@@ -45,6 +46,13 @@ const configurationEntrySchema: IJSONSchema = {
nls.localize('scope.resource.description', "Resource specific configuration, which can be configured in the User, Workspace or Folder settings.")
],
description: nls.localize('scope.description', "Scope in which the configuration is applicable. Available scopes are `window` and `resource`.")
},
enumDescriptions: {
type: 'array',
items: {
type: 'string',
},
description: nls.localize('scope.enumDescriptions', 'Descriptions for enum values')
}
}
}
@@ -105,7 +113,8 @@ configurationExtPoint.setHandler(extensions => {
validateProperties(configuration, extension);
configuration.id = extension.description.uuid || extension.description.id;
configuration.id = node.id || extension.description.id || extension.description.uuid;
configuration.contributedByExtension = true;
configuration.title = configuration.title || extension.description.displayName || extension.description.id;
configurations.push(configuration);
}
@@ -131,7 +140,17 @@ function validateProperties(configuration: IConfigurationNode, extension: IExten
}
for (let key in properties) {
const message = validateProperty(key);
if (message) {
delete properties[key];
extension.collector.warn(message);
continue;
}
const propertyConfiguration = configuration.properties[key];
if (!isObject(propertyConfiguration)) {
delete properties[key];
extension.collector.error(nls.localize('invalid.property', "'configuration.property' must be an object"));
continue;
}
if (propertyConfiguration.scope) {
if (propertyConfiguration.scope.toString() === 'application') {
propertyConfiguration.scope = ConfigurationScope.APPLICATION;
@@ -144,10 +163,6 @@ function validateProperties(configuration: IConfigurationNode, extension: IExten
propertyConfiguration.scope = ConfigurationScope.WINDOW;
}
propertyConfiguration.notMultiRootAdopted = !(extension.description.isBuiltin || (Array.isArray(extension.description.keywords) && extension.description.keywords.indexOf('multi-root ready') !== -1));
if (message) {
extension.collector.warn(message);
delete properties[key];
}
}
}
let subNodes = configuration.allOf;

View File

@@ -8,7 +8,7 @@ import { equals } from 'vs/base/common/objects';
import { compare, toValuesTree, IConfigurationChangeEvent, ConfigurationTarget, IConfigurationModel, IConfigurationOverrides } from 'vs/platform/configuration/common/configuration';
import { Configuration as BaseConfiguration, ConfigurationModelParser, ConfigurationChangeEvent, ConfigurationModel, AbstractConfigurationChangeEvent } from 'vs/platform/configuration/common/configurationModels';
import { Registry } from 'vs/platform/registry/common/platform';
import { IConfigurationRegistry, IConfigurationPropertySchema, Extensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry';
import { IConfigurationRegistry, IConfigurationPropertySchema, Extensions, ConfigurationScope, OVERRIDE_PROPERTY_PATTERN } from 'vs/platform/configuration/common/configurationRegistry';
import { IStoredWorkspaceFolder } from 'vs/platform/workspaces/common/workspaces';
import { Workspace } from 'vs/platform/workspace/common/workspace';
import { ResourceMap } from 'vs/base/common/map';
@@ -101,18 +101,27 @@ export class FolderSettingsModelParser extends ConfigurationModelParser {
}
private parseWorkspaceSettings(rawSettings: any): void {
const rawWorkspaceSettings = {};
const configurationProperties = Registry.as<IConfigurationRegistry>(Extensions.Configuration).getConfigurationProperties();
for (let key in rawSettings) {
const scope = this.getScope(key, configurationProperties);
if (this.scopes.indexOf(scope) !== -1) {
rawWorkspaceSettings[key] = rawSettings[key];
}
}
const rawWorkspaceSettings = this.filterByScope(rawSettings, configurationProperties, true);
const configurationModel = this.parseRaw(rawWorkspaceSettings);
this._settingsModel = new ConfigurationModel(configurationModel.contents, configurationModel.keys, configurationModel.overrides);
}
private filterByScope(properties: {}, configurationProperties: { [qualifiedKey: string]: IConfigurationPropertySchema }, filterOverriddenProperties: boolean): {} {
const result = {};
for (let key in properties) {
if (OVERRIDE_PROPERTY_PATTERN.test(key) && filterOverriddenProperties) {
result[key] = this.filterByScope(properties[key], configurationProperties, false);
} else {
const scope = this.getScope(key, configurationProperties);
if (this.scopes.indexOf(scope) !== -1) {
result[key] = properties[key];
}
}
}
return result;
}
private getScope(key: string, configurationProperties: { [qualifiedKey: string]: IConfigurationPropertySchema }): ConfigurationScope {
const propertySchema = configurationProperties[key];
return propertySchema ? propertySchema.scope : ConfigurationScope.WINDOW;
@@ -160,9 +169,7 @@ export class Configuration extends BaseConfiguration {
const { added, updated, removed } = compare(this.user, user);
let changedKeys = [...added, ...updated, ...removed];
if (changedKeys.length) {
const oldValues = changedKeys.map(key => this.getValue(key));
super.updateUserConfiguration(user);
changedKeys = changedKeys.filter((key, index) => !equals(oldValues[index], this.getValue(key)));
}
return new ConfigurationChangeEvent().change(changedKeys);
}
@@ -171,9 +178,7 @@ export class Configuration extends BaseConfiguration {
const { added, updated, removed } = compare(this.workspace, workspaceConfiguration);
let changedKeys = [...added, ...updated, ...removed];
if (changedKeys.length) {
const oldValues = changedKeys.map(key => this.getValue(key));
super.updateWorkspaceConfiguration(workspaceConfiguration);
changedKeys = changedKeys.filter((key, index) => !equals(oldValues[index], this.getValue(key)));
}
return new ConfigurationChangeEvent().change(changedKeys);
}
@@ -184,9 +189,7 @@ export class Configuration extends BaseConfiguration {
const { added, updated, removed } = compare(currentFolderConfiguration, folderConfiguration);
let changedKeys = [...added, ...updated, ...removed];
if (changedKeys.length) {
const oldValues = changedKeys.map(key => this.getValue(key, { resource }));
super.updateFolderConfiguration(resource, folderConfiguration);
changedKeys = changedKeys.filter((key, index) => !equals(oldValues[index], this.getValue(key, { resource })));
}
return new ConfigurationChangeEvent().change(changedKeys, resource);
} else {

View File

@@ -6,13 +6,14 @@
import URI from 'vs/base/common/uri';
import { createHash } from 'crypto';
import * as paths from 'vs/base/common/paths';
import * as resources from 'vs/base/common/resources';
import { TPromise } from 'vs/base/common/winjs.base';
import { Event, Emitter } from 'vs/base/common/event';
import * as pfs from 'vs/base/node/pfs';
import * as errors from 'vs/base/common/errors';
import * as collections from 'vs/base/common/collections';
import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle';
import { RunOnceScheduler } from 'vs/base/common/async';
import { RunOnceScheduler, Delayer } from 'vs/base/common/async';
import { FileChangeType, FileChangesEvent, IContent, IFileService } from 'vs/platform/files/common/files';
import { isLinux } from 'vs/base/common/platform';
import { ConfigWatcher } from 'vs/base/node/config';
@@ -258,35 +259,33 @@ export class NodeBasedFolderConfiguration extends AbstractFolderConfiguration {
export class FileServiceBasedFolderConfiguration extends AbstractFolderConfiguration {
private bulkContentFetchromise: TPromise<any>;
private workspaceFilePathToConfiguration: { [relativeWorkspacePath: string]: TPromise<IContent> };
private reloadConfigurationScheduler: RunOnceScheduler;
private readonly folderConfigurationPath: URI;
private readonly loadConfigurationDelayer: Delayer<{ resource: URI, value: string }[]> = new Delayer<{ resource: URI, value: string }[]>(50);
constructor(folder: URI, private configFolderRelativePath: string, workbenchState: WorkbenchState, private fileService: IFileService, from?: AbstractFolderConfiguration) {
super(folder, workbenchState, from);
this.folderConfigurationPath = folder.with({ path: paths.join(this.folder.path, configFolderRelativePath) });
this.workspaceFilePathToConfiguration = Object.create(null);
this.reloadConfigurationScheduler = this._register(new RunOnceScheduler(() => this._onDidChange.fire(), 50));
this._register(fileService.onFileChanges(e => this.handleWorkspaceFileEvents(e)));
}
protected loadFolderConfigurationContents(): TPromise<{ resource: URI, value: string }[]> {
// once: when invoked for the first time we fetch json files that contribute settings
if (!this.bulkContentFetchromise) {
this.bulkContentFetchromise = this.fileService.resolveFile(this.folderConfigurationPath)
.then(stat => {
if (stat.isDirectory && stat.children) {
stat.children
.filter(child => isFolderConfigurationFile(child.resource))
.forEach(child => this.workspaceFilePathToConfiguration[this.toFolderRelativePath(child.resource)] = this.fileService.resolveContent(child.resource).then(null, errors.onUnexpectedError));
}
}).then(null, err => [] /* never fail this call */);
}
return this.loadConfigurationDelayer.trigger(() => this.doLoadFolderConfigurationContents());
}
// on change: join on *all* configuration file promises so that we can merge them into a single configuration object. this
// happens whenever a config file changes, is deleted, or added
return this.bulkContentFetchromise.then(() => TPromise.join(this.workspaceFilePathToConfiguration).then(result => collections.values(result)));
private doLoadFolderConfigurationContents(): TPromise<{ resource: URI, value: string }[]> {
const workspaceFilePathToConfiguration: { [relativeWorkspacePath: string]: TPromise<IContent> } = Object.create(null);
const bulkContentFetchromise = this.fileService.resolveFile(this.folderConfigurationPath)
.then(stat => {
if (stat.isDirectory && stat.children) {
stat.children
.filter(child => isFolderConfigurationFile(child.resource))
.forEach(child => workspaceFilePathToConfiguration[this.toFolderRelativePath(child.resource)] = this.fileService.resolveContent(child.resource).then(null, errors.onUnexpectedError));
}
}).then(null, err => [] /* never fail this call */);
return bulkContentFetchromise.then(() => TPromise.join(collections.values(workspaceFilePathToConfiguration)));
}
private handleWorkspaceFileEvents(event: FileChangesEvent): void {
@@ -312,7 +311,6 @@ export class FileServiceBasedFolderConfiguration extends AbstractFolderConfigura
// Handle case where ".vscode" got deleted
if (isDeletedSettingsFolder) {
this.workspaceFilePathToConfiguration = Object.create(null);
affectedByChanges = true;
}
@@ -321,15 +319,10 @@ export class FileServiceBasedFolderConfiguration extends AbstractFolderConfigura
continue;
}
// insert 'fetch-promises' for add and update events and
// remove promises for delete events
switch (events[i].type) {
case FileChangeType.DELETED:
affectedByChanges = collections.remove(this.workspaceFilePathToConfiguration, folderRelativePath);
break;
case FileChangeType.UPDATED:
case FileChangeType.ADDED:
this.workspaceFilePathToConfiguration[folderRelativePath] = this.fileService.resolveContent(resource).then(null, errors.onUnexpectedError);
affectedByChanges = true;
}
}
@@ -345,7 +338,7 @@ export class FileServiceBasedFolderConfiguration extends AbstractFolderConfigura
return paths.normalize(relative(this.folderConfigurationPath.fsPath, resource.fsPath));
}
} else {
if (paths.isEqualOrParent(resource.path, this.folderConfigurationPath.path, true /* ignorecase */)) {
if (resources.isEqualOrParent(resource, this.folderConfigurationPath, resources.hasToIgnoreCase(resource))) {
return paths.normalize(relative(this.folderConfigurationPath.path, resource.path));
}
}
@@ -497,4 +490,4 @@ export class FolderConfiguration extends Disposable implements IFolderConfigurat
}
return TPromise.as(null);
}
}
}

View File

@@ -28,7 +28,7 @@ import { IFileService } from 'vs/platform/files/common/files';
import { ITextModelService, ITextEditorModel } from 'vs/editor/common/services/resolverService';
import { OVERRIDE_PROPERTY_PATTERN, IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { ITextModel } from 'vs/editor/common/model';
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
@@ -133,7 +133,7 @@ export class ConfigurationEditingService {
@ITextFileService private textFileService: ITextFileService,
@INotificationService private notificationService: INotificationService,
@ICommandService private commandService: ICommandService,
@IWorkbenchEditorService private editorService: IWorkbenchEditorService
@IEditorService private editorService: IEditorService
) {
this.queue = new Queue<void>();
}
@@ -371,7 +371,7 @@ export class ConfigurationEditingService {
return false;
}
const parseErrors: json.ParseError[] = [];
json.parse(model.getValue(), parseErrors, { allowTrailingComma: true });
json.parse(model.getValue(), parseErrors);
return parseErrors.length > 0;
}

View File

@@ -16,8 +16,8 @@ import { Queue } from 'vs/base/common/async';
import { stat, writeFile } from 'vs/base/node/pfs';
import { IJSONContributionRegistry, Extensions as JSONExtensions } from 'vs/platform/jsonschemas/common/jsonContributionRegistry';
import { IWorkspaceContextService, Workspace, WorkbenchState, IWorkspaceFolder, toWorkspaceFolders, IWorkspaceFoldersChangeEvent, WorkspaceFolder } from 'vs/platform/workspace/common/workspace';
import { isLinux, isWindows, isMacintosh } from 'vs/base/common/platform';
import { IFileService } from 'vs/platform/files/common/files';
import { isLinux } from 'vs/base/common/platform';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { ConfigurationChangeEvent, ConfigurationModel, DefaultConfigurationModel } from 'vs/platform/configuration/common/configurationModels';
import { IConfigurationChangeEvent, ConfigurationTarget, IConfigurationOverrides, keyFromOverrideIdentifier, isConfigurationOverrides, IConfigurationData } from 'vs/platform/configuration/common/configuration';
@@ -26,7 +26,7 @@ import { IWorkspaceConfigurationService, FOLDER_CONFIG_FOLDER_NAME, defaultSetti
import { Registry } from 'vs/platform/registry/common/platform';
import { IConfigurationNode, IConfigurationRegistry, Extensions, IConfigurationPropertySchema, allSettings, windowSettings, resourceSettings, applicationSettings } from 'vs/platform/configuration/common/configurationRegistry';
import { createHash } from 'crypto';
import { getWorkspaceLabel, IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier, IStoredWorkspaceFolder, isStoredWorkspaceFolder, IWorkspaceFolderCreationData } from 'vs/platform/workspaces/common/workspaces';
import { getWorkspaceLabel, IWorkspaceIdentifier, isWorkspaceIdentifier, IStoredWorkspaceFolder, isStoredWorkspaceFolder, IWorkspaceFolderCreationData, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
import { IWindowConfiguration } from 'vs/platform/windows/common/windows';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { ICommandService } from 'vs/platform/commands/common/commands';
@@ -38,9 +38,10 @@ import { JSONEditingService } from 'vs/workbench/services/configuration/node/jso
import { Schemas } from 'vs/base/common/network';
import { massageFolderPathForWorkspace } from 'vs/platform/workspaces/node/workspaces';
import { UserConfiguration } from 'vs/platform/configuration/node/configuration';
import { getBaseLabel } from 'vs/base/common/labels';
import { IJSONSchema, IJSONSchemaMap } from 'vs/base/common/jsonSchema';
import { localize } from 'vs/nls';
import { isEqual, hasToIgnoreCase } from 'vs/base/common/resources';
import { IUriDisplayService } from 'vs/platform/uriDisplay/common/uriDisplay';
export class WorkspaceService extends Disposable implements IWorkspaceConfigurationService, IWorkspaceContextService {
@@ -68,6 +69,7 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat
public readonly onDidChangeWorkbenchState: Event<WorkbenchState> = this._onDidChangeWorkbenchState.event;
private fileService: IFileService;
private uriDisplayService: IUriDisplayService;
private configurationEditingService: ConfigurationEditingService;
private jsonEditingService: JSONEditingService;
@@ -80,7 +82,7 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat
this._register(this.userConfiguration.onDidChangeConfiguration(() => this.onUserConfigurationChanged()));
this._register(this.workspaceConfiguration.onDidUpdateConfiguration(() => this.onWorkspaceConfigurationChanged()));
this._register(Registry.as<IConfigurationRegistry>(Extensions.Configuration).onDidRegisterConfiguration(e => this.registerConfigurationSchemas()));
this._register(Registry.as<IConfigurationRegistry>(Extensions.Configuration).onDidSchemaChange(e => this.registerConfigurationSchemas()));
this._register(Registry.as<IConfigurationRegistry>(Extensions.Configuration).onDidRegisterConfiguration(configurationProperties => this.onDefaultConfigurationChanged(configurationProperties)));
this.workspaceEditingQueue = new Queue<void>();
@@ -131,7 +133,7 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat
public isCurrentWorkspace(workspaceIdentifier: ISingleFolderWorkspaceIdentifier | IWorkspaceIdentifier): boolean {
switch (this.getWorkbenchState()) {
case WorkbenchState.FOLDER:
return isSingleFolderWorkspaceIdentifier(workspaceIdentifier) && this.pathEquals(this.workspace.folders[0].uri.fsPath, workspaceIdentifier);
return isSingleFolderWorkspaceIdentifier(workspaceIdentifier) && isEqual(workspaceIdentifier, this.workspace.folders[0].uri, hasToIgnoreCase(workspaceIdentifier));
case WorkbenchState.WORKSPACE:
return isWorkspaceIdentifier(workspaceIdentifier) && this.workspace.id === workspaceIdentifier.id;
}
@@ -295,9 +297,9 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat
return this._configuration.keys();
}
initialize(arg: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IWindowConfiguration): TPromise<any> {
initialize(arg: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IWindowConfiguration, postInitialisationTask: () => void = () => null): TPromise<any> {
return this.createWorkspace(arg)
.then(workspace => this.updateWorkspaceAndInitializeConfiguration(workspace));
.then(workspace => this.updateWorkspaceAndInitializeConfiguration(workspace, postInitialisationTask));
}
acquireFileService(fileService: IFileService): void {
@@ -317,12 +319,16 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat
});
}
acquireUriDisplayService(uriDisplayService: IUriDisplayService): void {
this.uriDisplayService = uriDisplayService;
}
acquireInstantiationService(instantiationService: IInstantiationService): void {
this.configurationEditingService = instantiationService.createInstance(ConfigurationEditingService);
this.jsonEditingService = instantiationService.createInstance(JSONEditingService);
}
private createWorkspace(arg: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IWindowConfiguration): TPromise<Workspace> {
private createWorkspace(arg: IWorkspaceIdentifier | URI | IWindowConfiguration): TPromise<Workspace> {
if (isWorkspaceIdentifier(arg)) {
return this.createMulitFolderWorkspace(arg);
}
@@ -340,20 +346,35 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat
.then(() => {
const workspaceFolders = toWorkspaceFolders(this.workspaceConfiguration.getFolders(), URI.file(dirname(workspaceConfigPath.fsPath)));
const workspaceId = workspaceIdentifier.id;
const workspaceName = getWorkspaceLabel({ id: workspaceId, configPath: workspaceConfigPath.fsPath }, this.environmentService);
const workspaceName = getWorkspaceLabel({ id: workspaceId, configPath: workspaceConfigPath.fsPath }, this.environmentService, this.uriDisplayService);
return new Workspace(workspaceId, workspaceName, workspaceFolders, workspaceConfigPath);
});
}
private createSingleFolderWorkspace(singleFolderWorkspaceIdentifier: ISingleFolderWorkspaceIdentifier): TPromise<Workspace> {
const folderPath = URI.file(singleFolderWorkspaceIdentifier);
return stat(folderPath.fsPath)
.then(workspaceStat => {
const ctime = isLinux ? workspaceStat.ino : workspaceStat.birthtime.getTime(); // On Linux, birthtime is ctime, so we cannot use it! We use the ino instead!
const id = createHash('md5').update(folderPath.fsPath).update(ctime ? String(ctime) : '').digest('hex');
const folder = URI.file(folderPath.fsPath);
return new Workspace(id, getBaseLabel(folder), toWorkspaceFolders([{ path: folder.fsPath }]), null, ctime);
});
private createSingleFolderWorkspace(folder: URI): TPromise<Workspace> {
if (folder.scheme === Schemas.file) {
return stat(folder.fsPath)
.then(workspaceStat => {
let ctime: number;
if (isLinux) {
ctime = workspaceStat.ino; // Linux: birthtime is ctime, so we cannot use it! We use the ino instead!
} else if (isMacintosh) {
ctime = workspaceStat.birthtime.getTime(); // macOS: birthtime is fine to use as is
} else if (isWindows) {
if (typeof workspaceStat.birthtimeMs === 'number') {
ctime = Math.floor(workspaceStat.birthtimeMs); // Windows: fix precision issue in node.js 8.x to get 7.x results (see https://github.com/nodejs/node/issues/19897)
} else {
ctime = workspaceStat.birthtime.getTime();
}
}
const id = createHash('md5').update(folder.fsPath).update(ctime ? String(ctime) : '').digest('hex');
return new Workspace(id, getWorkspaceLabel(folder, this.environmentService, this.uriDisplayService), toWorkspaceFolders([{ path: folder.fsPath }]), null, ctime);
});
} else {
const id = createHash('md5').update(folder.toString()).digest('hex');
return TPromise.as(new Workspace(id, getWorkspaceLabel(folder, this.environmentService, this.uriDisplayService), toWorkspaceFolders([{ uri: folder.toString() }]), null));
}
}
private createEmptyWorkspace(configuration: IWindowConfiguration): TPromise<Workspace> {
@@ -361,7 +382,7 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat
return TPromise.as(new Workspace(id));
}
private updateWorkspaceAndInitializeConfiguration(workspace: Workspace): TPromise<void> {
private updateWorkspaceAndInitializeConfiguration(workspace: Workspace, postInitialisationTask: () => void): TPromise<void> {
const hasWorkspaceBefore = !!this.workspace;
let previousState: WorkbenchState;
let previousWorkspacePath: string;
@@ -377,6 +398,9 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat
}
return this.initializeConfiguration().then(() => {
postInitialisationTask(); // Post initialisation task should be run before triggering events.
// Trigger changes after configuration initialization so that configuration is up to date.
if (hasWorkspaceBefore) {
const newState = this.getWorkbenchState();
@@ -667,15 +691,6 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat
}
return {};
}
private pathEquals(path1: string, path2: string): boolean {
if (!isLinux) {
path1 = path1.toLowerCase();
path2 = path2.toLowerCase();
}
return path1 === path2;
}
}
interface IExportedConfigurationNode {

View File

@@ -95,7 +95,7 @@ export class JSONEditingService implements IJSONEditingService {
private hasParseErrors(model: ITextModel): boolean {
const parseErrors: json.ParseError[] = [];
json.parse(model.getValue(), parseErrors, { allowTrailingComma: true });
json.parse(model.getValue(), parseErrors);
return parseErrors.length > 0;
}

View File

@@ -30,7 +30,8 @@ suite('FolderSettingsModelParser', () => {
'FolderSettingsModelParser.resource': {
'type': 'string',
'default': 'isSet',
scope: ConfigurationScope.RESOURCE
scope: ConfigurationScope.RESOURCE,
overridable: true
},
'FolderSettingsModelParser.application': {
'type': 'string',
@@ -57,6 +58,14 @@ suite('FolderSettingsModelParser', () => {
assert.deepEqual(testObject.configurationModel.contents, { 'FolderSettingsModelParser': { 'resource': 'resource' } });
});
test('parse overridable resource settings', () => {
const testObject = new FolderSettingsModelParser('settings', [ConfigurationScope.RESOURCE]);
testObject.parse(JSON.stringify({ '[json]': { 'FolderSettingsModelParser.window': 'window', 'FolderSettingsModelParser.resource': 'resource', 'FolderSettingsModelParser.application': 'executable' } }));
assert.deepEqual(testObject.configurationModel.overrides, [{ 'contents': { 'FolderSettingsModelParser': { 'resource': 'resource' } }, 'identifiers': ['json'] }]);
});
test('reprocess folder settings excludes application setting', () => {
const testObject = new FolderSettingsModelParser('settings', [ConfigurationScope.RESOURCE, ConfigurationScope.WINDOW]);

View File

@@ -38,6 +38,7 @@ import { mkdirp } from 'vs/base/node/pfs';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { CommandService } from 'vs/workbench/services/commands/common/commandService';
import URI from 'vs/base/common/uri';
class SettingsTestEnvironmentService extends EnvironmentService {
@@ -104,7 +105,7 @@ suite('ConfigurationEditingService', () => {
instantiationService.stub(IEnvironmentService, environmentService);
const workspaceService = new WorkspaceService(environmentService);
instantiationService.stub(IWorkspaceContextService, workspaceService);
return workspaceService.initialize(noWorkspace ? {} as IWindowConfiguration : workspaceDir).then(() => {
return workspaceService.initialize(noWorkspace ? {} as IWindowConfiguration : URI.file(workspaceDir)).then(() => {
instantiationService.stub(IConfigurationService, workspaceService);
instantiationService.stub(IFileService, new FileService(workspaceService, TestEnvironmentService, new TestTextResourceConfigurationService(), new TestConfigurationService(), new TestLifecycleService(), new TestStorageService(), new TestNotificationService(), { disableWatcher: true }));
instantiationService.stub(ITextFileService, instantiationService.createInstance(TestTextFileService));

View File

@@ -13,9 +13,20 @@ export const IConfigurationResolverService = createDecorator<IConfigurationResol
export interface IConfigurationResolverService {
_serviceBrand: any;
resolve(root: IWorkspaceFolder, value: string): string;
resolve(root: IWorkspaceFolder, value: string[]): string[];
resolve(root: IWorkspaceFolder, value: IStringDictionary<string>): IStringDictionary<string>;
resolveAny<T>(root: IWorkspaceFolder, value: T, commandMapping?: IStringDictionary<string>): T;
executeCommandVariables(value: any, variables: IStringDictionary<string>): TPromise<IStringDictionary<string>>;
resolve(folder: IWorkspaceFolder, value: string): string;
resolve(folder: IWorkspaceFolder, value: string[]): string[];
resolve(folder: IWorkspaceFolder, value: IStringDictionary<string>): IStringDictionary<string>;
/**
* Recursively resolves all variables in the given config and returns a copy of it with substituted values.
* Command variables are only substituted if a "commandValueMapping" dictionary is given and if it contains an entry for the command.
*/
resolveAny(folder: IWorkspaceFolder, config: any, commandValueMapping?: IStringDictionary<string>): any;
/**
* Recursively resolves all variables (including commands) in the given config and returns a copy of it with substituted values.
* If a "variables" dictionary (with names -> command ids) is given,
* command variables are first mapped through it before being resolved.
*/
resolveWithCommands(folder: IWorkspaceFolder, config: any, variables?: IStringDictionary<string>): TPromise<any>;
}

View File

@@ -4,37 +4,35 @@
*--------------------------------------------------------------------------------------------*/
import uri from 'vs/base/common/uri';
import * as nls from 'vs/nls';
import * as paths from 'vs/base/common/paths';
import * as platform from 'vs/base/common/platform';
import { Schemas } from 'vs/base/common/network';
import { TPromise } from 'vs/base/common/winjs.base';
import { sequence } from 'vs/base/common/async';
import { toResource } from 'vs/workbench/common/editor';
import { IStringDictionary } from 'vs/base/common/collections';
import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver';
import { IStringDictionary, size } from 'vs/base/common/collections';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { IWorkspaceFolder, IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IProcessEnvironment } from 'vs/base/common/platform';
import { VariableResolver } from 'vs/workbench/services/configurationResolver/node/variableResolver';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { AbstractVariableResolverService } from 'vs/workbench/services/configurationResolver/node/variableResolver';
import { isCodeEditor } from 'vs/editor/browser/editorBrowser';
import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput';
import { isUndefinedOrNull } from 'vs/base/common/types';
export class ConfigurationResolverService implements IConfigurationResolverService {
_serviceBrand: any;
private resolver: VariableResolver;
export class ConfigurationResolverService extends AbstractVariableResolverService {
constructor(
envVariables: IProcessEnvironment,
@IWorkbenchEditorService editorService: IWorkbenchEditorService,
envVariables: platform.IProcessEnvironment,
@IEditorService editorService: IEditorService,
@IEnvironmentService environmentService: IEnvironmentService,
@IConfigurationService configurationService: IConfigurationService,
@ICommandService private commandService: ICommandService,
@IWorkspaceContextService workspaceContextService: IWorkspaceContextService
) {
this.resolver = new VariableResolver({
super({
getFolderUri: (folderName: string): uri => {
const folder = workspaceContextService.getWorkspace().folders.filter(f => f.name === folderName).pop();
return folder ? folder.uri : undefined;
@@ -49,59 +47,66 @@ export class ConfigurationResolverService implements IConfigurationResolverServi
return environmentService['execPath'];
},
getFilePath: (): string | undefined => {
let input = editorService.getActiveEditorInput();
if (input instanceof DiffEditorInput) {
input = input.modifiedInput;
let activeEditor = editorService.activeEditor;
if (activeEditor instanceof DiffEditorInput) {
activeEditor = activeEditor.modifiedInput;
}
const fileResource = toResource(input, { filter: Schemas.file });
const fileResource = toResource(activeEditor, { filter: Schemas.file });
if (!fileResource) {
return undefined;
}
return paths.normalize(fileResource.fsPath, true);
},
getSelectedText: (): string | undefined => {
const activeEditor = editorService.getActiveEditor();
if (activeEditor) {
const editorControl = (<ICodeEditor>activeEditor.getControl());
if (editorControl) {
const editorModel = editorControl.getModel();
const editorSelection = editorControl.getSelection();
if (editorModel && editorSelection) {
return editorModel.getValueInRange(editorSelection);
}
const activeTextEditorWidget = editorService.activeTextEditorWidget;
if (isCodeEditor(activeTextEditorWidget)) {
const editorModel = activeTextEditorWidget.getModel();
const editorSelection = activeTextEditorWidget.getSelection();
if (editorModel && editorSelection) {
return editorModel.getValueInRange(editorSelection);
}
}
return undefined;
},
getLineNumber: (): string => {
const activeEditor = editorService.getActiveEditor();
if (activeEditor) {
const editorControl = (<ICodeEditor>activeEditor.getControl());
if (editorControl) {
const lineNumber = editorControl.getSelection().positionLineNumber;
return String(lineNumber);
}
const activeTextEditorWidget = editorService.activeTextEditorWidget;
if (isCodeEditor(activeTextEditorWidget)) {
const lineNumber = activeTextEditorWidget.getSelection().positionLineNumber;
return String(lineNumber);
}
return undefined;
}
}, envVariables);
}
public resolve(root: IWorkspaceFolder, value: string): string;
public resolve(root: IWorkspaceFolder, value: string[]): string[];
public resolve(root: IWorkspaceFolder, value: IStringDictionary<string>): IStringDictionary<string>;
public resolve(root: IWorkspaceFolder, value: any): any {
return this.resolver.resolveAny(root ? root.uri : undefined, value);
}
public resolveWithCommands(folder: IWorkspaceFolder, config: any, variables?: IStringDictionary<string>): TPromise<any> {
public resolveAny(root: IWorkspaceFolder, value: any, commandValueMapping?: IStringDictionary<string>): any {
return this.resolver.resolveAny(root ? root.uri : undefined, value, commandValueMapping);
// then substitute remaining variables in VS Code core
config = this.resolveAny(folder, config);
// now evaluate command variables (which might have a UI)
return this.executeCommandVariables(config, variables).then(commandValueMapping => {
if (!commandValueMapping) { // cancelled by user
return null;
}
// finally substitute evaluated command variables (if there are any)
if (size<string>(commandValueMapping) > 0) {
return this.resolveAny(folder, config, commandValueMapping);
} else {
return config;
}
});
}
/**
* Finds and executes all command variables (see #6569)
* Finds and executes all command variables in the given configuration and returns their values as a dictionary.
* Please note: this method does not substitute the command variables (so the configuration is not modified).
* The returned dictionary can be passed to "resolvePlatform" for the substitution.
* See #6569.
*/
public executeCommandVariables(configuration: any, variableToCommandMap: IStringDictionary<string>): TPromise<IStringDictionary<string>> {
private executeCommandVariables(configuration: any, variableToCommandMap: IStringDictionary<string>): TPromise<IStringDictionary<string>> {
if (!configuration) {
return TPromise.as(null);
@@ -136,20 +141,22 @@ export class ConfigurationResolverService implements IConfigurationResolverServi
let cancelled = false;
const commandValueMapping: IStringDictionary<string> = Object.create(null);
const factory: { (): TPromise<any> }[] = commands.map(interactiveVariable => {
const factory: { (): TPromise<any> }[] = commands.map(commandVariable => {
return () => {
let commandId = variableToCommandMap ? variableToCommandMap[interactiveVariable] : null;
let commandId = variableToCommandMap ? variableToCommandMap[commandVariable] : null;
if (!commandId) {
// Just launch any command if the interactive variable is not contributed by the adapter #12735
commandId = interactiveVariable;
commandId = commandVariable;
}
return this.commandService.executeCommand<string>(commandId, configuration).then(result => {
if (result) {
commandValueMapping[interactiveVariable] = result;
} else {
if (typeof result === 'string') {
commandValueMapping[commandVariable] = result;
} else if (isUndefinedOrNull(result)) {
cancelled = true;
} else {
throw new Error(nls.localize('stringsOnlySupported', "Command '{0}' did not return a string result. Only strings are supported as results for commands used for variable substitution.", commandVariable));
}
});
};

View File

@@ -5,14 +5,18 @@
import * as paths from 'vs/base/common/paths';
import * as types from 'vs/base/common/types';
import * as objects from 'vs/base/common/objects';
import { IStringDictionary } from 'vs/base/common/collections';
import { relative } from 'path';
import { IProcessEnvironment, isWindows } from 'vs/base/common/platform';
import { IProcessEnvironment, isWindows, isMacintosh, isLinux } from 'vs/base/common/platform';
import { normalizeDriveLetter } from 'vs/base/common/labels';
import { localize } from 'vs/nls';
import uri from 'vs/base/common/uri';
import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver';
import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
import { TPromise } from 'vs/base/common/winjs.base';
export interface IVariableAccessor {
export interface IVariableResolveContext {
getFolderUri(folderName: string): uri | undefined;
getWorkspaceFolderCount(): number;
getConfigurationValue(folderUri: uri, section: string): string | undefined;
@@ -22,46 +26,78 @@ export interface IVariableAccessor {
getLineNumber(): string;
}
export class VariableResolver {
export class AbstractVariableResolverService implements IConfigurationResolverService {
static VARIABLE_REGEXP = /\$\{(.*?)\}/g;
private envVariables: IProcessEnvironment;
_serviceBrand: any;
constructor(
private accessor: IVariableAccessor,
envVariables: IProcessEnvironment
private _context: IVariableResolveContext,
private _envVariables: IProcessEnvironment = process.env
) {
if (isWindows) {
this.envVariables = Object.create(null);
Object.keys(envVariables).forEach(key => {
this.envVariables[key.toLowerCase()] = envVariables[key];
this._envVariables = Object.create(null);
Object.keys(_envVariables).forEach(key => {
this._envVariables[key.toLowerCase()] = _envVariables[key];
});
} else {
this.envVariables = envVariables;
}
}
resolveAny(folderUri: uri, value: any, commandValueMapping?: IStringDictionary<string>): any {
public resolve(root: IWorkspaceFolder, value: string): string;
public resolve(root: IWorkspaceFolder, value: string[]): string[];
public resolve(root: IWorkspaceFolder, value: IStringDictionary<string>): IStringDictionary<string>;
public resolve(root: IWorkspaceFolder, value: any): any {
return this.recursiveResolve(root ? root.uri : undefined, value);
}
public resolveAny(workspaceFolder: IWorkspaceFolder, config: any, commandValueMapping?: IStringDictionary<string>): any {
const result = objects.deepClone(config) as any;
// hoist platform specific attributes to top level
if (isWindows && result.windows) {
Object.keys(result.windows).forEach(key => result[key] = result.windows[key]);
} else if (isMacintosh && result.osx) {
Object.keys(result.osx).forEach(key => result[key] = result.osx[key]);
} else if (isLinux && result.linux) {
Object.keys(result.linux).forEach(key => result[key] = result.linux[key]);
}
// delete all platform specific sections
delete result.windows;
delete result.osx;
delete result.linux;
// substitute all variables recursively in string values
return this.recursiveResolve(workspaceFolder ? workspaceFolder.uri : undefined, result, commandValueMapping);
}
public resolveWithCommands(folder: IWorkspaceFolder, config: any): TPromise<any> {
throw new Error('resolveWithCommands not implemented.');
}
private recursiveResolve(folderUri: uri, value: any, commandValueMapping?: IStringDictionary<string>): any {
if (types.isString(value)) {
return this.resolve(folderUri, value, commandValueMapping);
return this.resolveString(folderUri, value, commandValueMapping);
} else if (types.isArray(value)) {
return value.map(s => this.resolveAny(folderUri, s, commandValueMapping));
return value.map(s => this.recursiveResolve(folderUri, s, commandValueMapping));
} else if (types.isObject(value)) {
let result: IStringDictionary<string | IStringDictionary<string> | string[]> = Object.create(null);
Object.keys(value).forEach(key => {
result[key] = this.resolveAny(folderUri, value[key], commandValueMapping);
const resolvedKey = this.resolveString(folderUri, key, commandValueMapping);
result[resolvedKey] = this.recursiveResolve(folderUri, value[key], commandValueMapping);
});
return result;
}
return value;
}
resolve(folderUri: uri, value: string, commandValueMapping: IStringDictionary<string>): string {
private resolveString(folderUri: uri, value: string, commandValueMapping: IStringDictionary<string>): string {
const filePath = this.accessor.getFilePath();
const filePath = this._context.getFilePath();
return value.replace(VariableResolver.VARIABLE_REGEXP, (match: string, variable: string) => {
return value.replace(AbstractVariableResolverService.VARIABLE_REGEXP, (match: string, variable: string) => {
let argument: string;
const parts = variable.split(':');
@@ -77,7 +113,7 @@ export class VariableResolver {
if (isWindows) {
argument = argument.toLowerCase();
}
const env = this.envVariables[argument];
const env = this._envVariables[argument];
if (types.isString(env)) {
return env;
}
@@ -88,7 +124,7 @@ export class VariableResolver {
case 'config':
if (argument) {
const config = this.accessor.getConfigurationValue(folderUri, argument);
const config = this._context.getConfigurationValue(folderUri, argument);
if (types.isUndefinedOrNull(config)) {
throw new Error(localize('configNotFound', "'{0}' can not be resolved because setting '{1}' not found.", match, argument));
}
@@ -119,7 +155,7 @@ export class VariableResolver {
case 'workspaceFolderBasename':
case 'relativeFile':
if (argument) {
const folder = this.accessor.getFolderUri(argument);
const folder = this._context.getFolderUri(argument);
if (folder) {
folderUri = folder;
} else {
@@ -127,7 +163,7 @@ export class VariableResolver {
}
}
if (!folderUri) {
if (this.accessor.getWorkspaceFolderCount() > 1) {
if (this._context.getWorkspaceFolderCount() > 1) {
throw new Error(localize('canNotResolveWorkspaceFolderMultiRoot', "'{0}' can not be resolved in a multi folder workspace. Scope this variable using ':' and a workspace folder name.", match));
}
throw new Error(localize('canNotResolveWorkspaceFolder', "'{0}' can not be resolved. Please open a folder.", match));
@@ -166,14 +202,14 @@ export class VariableResolver {
return paths.basename(folderUri.fsPath);
case 'lineNumber':
const lineNumber = this.accessor.getLineNumber();
const lineNumber = this._context.getLineNumber();
if (lineNumber) {
return lineNumber;
}
throw new Error(localize('canNotResolveLineNumber', "'{0}' can not be resolved. Make sure to have a line selected in the active editor.", match));
case 'selectedText':
const selectedText = this.accessor.getSelectedText();
const selectedText = this._context.getSelectedText();
if (selectedText) {
return selectedText;
}
@@ -202,7 +238,7 @@ export class VariableResolver {
return basename.slice(0, basename.length - paths.extname(basename).length);
case 'execPath':
const ep = this.accessor.getExecPath();
const ep = this._context.getExecPath();
if (ep) {
return ep;
}

View File

@@ -14,6 +14,7 @@ import { ConfigurationResolverService } from 'vs/workbench/services/configuratio
import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
import { TestEnvironmentService, TestEditorService, TestContextService } from 'vs/workbench/test/workbenchTestServices';
import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService';
import { Disposable } from 'vs/base/common/lifecycle';
suite('Configuration Resolver Service', () => {
let configurationResolverService: IConfigurationResolverService;
@@ -50,13 +51,14 @@ suite('Configuration Resolver Service', () => {
assert.strictEqual(configurationResolverService.resolve(workspace, 'abc ${workspaceRootFolderName} xyz'), 'abc workspaceLocation xyz');
});
test('current selected line number', () => {
assert.strictEqual(configurationResolverService.resolve(workspace, 'abc ${lineNumber} xyz'), `abc ${editorService.mockLineNumber} xyz`);
});
// TODO@isidor mock the editor service properly
// test('current selected line number', () => {
// assert.strictEqual(configurationResolverService.resolve(workspace, 'abc ${lineNumber} xyz'), `abc ${editorService.mockLineNumber} xyz`);
// });
test('current selected text', () => {
assert.strictEqual(configurationResolverService.resolve(workspace, 'abc ${selectedText} xyz'), `abc ${editorService.mockSelectedText} xyz`);
});
// test('current selected text', () => {
// assert.strictEqual(configurationResolverService.resolve(workspace, 'abc ${selectedText} xyz'), `abc ${editorService.mockSelectedText} xyz`);
// });
test('substitute many', () => {
if (platform.isWindows) {
@@ -82,6 +84,18 @@ suite('Configuration Resolver Service', () => {
}
});
// test('substitute keys and values in object', () => {
// const myObject = {
// '${workspaceRootFolderName}': '${lineNumber}',
// 'hey ${env:key1} ': '${workspaceRootFolderName}'
// };
// assert.deepEqual(configurationResolverService.resolve(workspace, myObject), {
// 'workspaceLocation': `${editorService.mockLineNumber}`,
// 'hey Value for key1 ': 'workspaceLocation'
// });
// });
test('substitute one env variable using platform case sensitivity', () => {
if (platform.isWindows) {
assert.strictEqual(configurationResolverService.resolve(workspace, '${env:key1} - ${env:Key1}'), 'Value for key1 - Value for key1');
@@ -226,29 +240,25 @@ suite('Configuration Resolver Service', () => {
assert.throws(() => service.resolve(workspace, 'abc ${config:editor.none.none2} xyz'));
});
test('interactive variable simple', () => {
test('a single command variable', () => {
const configuration = {
'name': 'Attach to Process',
'type': 'node',
'request': 'attach',
'processId': '${command:interactiveVariable1}',
'processId': '${command:command1}',
'port': 5858,
'sourceMaps': false,
'outDir': null
};
const interactiveVariables = Object.create(null);
interactiveVariables['interactiveVariable1'] = 'command1';
interactiveVariables['interactiveVariable2'] = 'command2';
configurationResolverService.executeCommandVariables(configuration, interactiveVariables).then(mapping => {
const result = configurationResolverService.resolveAny(undefined, configuration, mapping);
return configurationResolverService.resolveWithCommands(undefined, configuration).then(result => {
assert.deepEqual(result, {
'name': 'Attach to Process',
'type': 'node',
'request': 'attach',
'processId': 'command1',
'processId': 'command1-result',
'port': 5858,
'sourceMaps': false,
'outDir': null
@@ -258,43 +268,96 @@ suite('Configuration Resolver Service', () => {
});
});
test('interactive variable complex', () => {
test('an old style command variable', () => {
const configuration = {
'name': 'Attach to Process',
'type': 'node',
'request': 'attach',
'processId': '${command:interactiveVariable1}',
'port': '${command:interactiveVariable2}',
'processId': '${command:commandVariable1}',
'port': 5858,
'sourceMaps': false,
'outDir': 'src/${command:interactiveVariable2}',
'env': {
'processId': '__${command:interactiveVariable2}__',
}
'outDir': null
};
const interactiveVariables = Object.create(null);
interactiveVariables['interactiveVariable1'] = 'command1';
interactiveVariables['interactiveVariable2'] = 'command2';
const commandVariables = Object.create(null);
commandVariables['commandVariable1'] = 'command1';
configurationResolverService.executeCommandVariables(configuration, interactiveVariables).then(mapping => {
const result = configurationResolverService.resolveAny(undefined, configuration, mapping);
return configurationResolverService.resolveWithCommands(undefined, configuration, commandVariables).then(result => {
assert.deepEqual(result, {
'name': 'Attach to Process',
'type': 'node',
'request': 'attach',
'processId': 'command1',
'port': 'command2',
'processId': 'command1-result',
'port': 5858,
'sourceMaps': false,
'outDir': 'src/command2',
'outDir': null
});
assert.equal(1, mockCommandService.callCount);
});
});
test('multiple new and old-style command variables', () => {
const configuration = {
'name': 'Attach to Process',
'type': 'node',
'request': 'attach',
'processId': '${command:commandVariable1}',
'pid': '${command:command2}',
'sourceMaps': false,
'outDir': 'src/${command:command2}',
'env': {
'processId': '__${command:command2}__',
}
};
const commandVariables = Object.create(null);
commandVariables['commandVariable1'] = 'command1';
return configurationResolverService.resolveWithCommands(undefined, configuration, commandVariables).then(result => {
assert.deepEqual(result, {
'name': 'Attach to Process',
'type': 'node',
'request': 'attach',
'processId': 'command1-result',
'pid': 'command2-result',
'sourceMaps': false,
'outDir': 'src/command2-result',
'env': {
'processId': '__command2__',
'processId': '__command2-result__',
}
});
assert.equal(2, mockCommandService.callCount);
});
});
test('a command variable that relies on resolved env vars', () => {
const configuration = {
'name': 'Attach to Process',
'type': 'node',
'request': 'attach',
'processId': '${command:commandVariable1}',
'value': '${env:key1}'
};
const commandVariables = Object.create(null);
commandVariables['commandVariable1'] = 'command1';
return configurationResolverService.resolveWithCommands(undefined, configuration, commandVariables).then(result => {
assert.deepEqual(result, {
'name': 'Attach to Process',
'type': 'node',
'request': 'attach',
'processId': 'Value for key1',
'value': 'Value for key1'
});
assert.equal(1, mockCommandService.callCount);
});
});
});
@@ -329,9 +392,17 @@ class MockCommandService implements ICommandService {
public _serviceBrand: any;
public callCount = 0;
onWillExecuteCommand = () => ({ dispose: () => { } });
onWillExecuteCommand = () => Disposable.None;
public executeCommand(commandId: string, ...args: any[]): TPromise<any> {
this.callCount++;
return TPromise.as(commandId);
let result = `${commandId}-result`;
if (args.length >= 1) {
if (args[0] && args[0].value) {
result = args[0].value;
}
}
return TPromise.as(result);
}
}

View File

@@ -12,37 +12,45 @@ import * as dom from 'vs/base/browser/dom';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { remote, webFrame } from 'electron';
import { unmnemonicLabel } from 'vs/base/common/labels';
import { Event, Emitter } from 'vs/base/common/event';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { IContextMenuDelegate, ContextSubMenu, IEvent } from 'vs/base/browser/contextmenu';
import { once } from 'vs/base/common/functional';
import { Disposable } from 'vs/base/common/lifecycle';
export class ContextMenuService implements IContextMenuService {
export class ContextMenuService extends Disposable implements IContextMenuService {
public _serviceBrand: any;
private _onDidContextMenu = new Emitter<void>();
_serviceBrand: any;
private _onDidContextMenu = this._register(new Emitter<void>());
get onDidContextMenu(): Event<void> { return this._onDidContextMenu.event; }
constructor(
@INotificationService private notificationService: INotificationService,
@ITelemetryService private telemetryService: ITelemetryService,
@IKeybindingService private keybindingService: IKeybindingService
) {
super();
}
public get onDidContextMenu(): Event<void> {
return this._onDidContextMenu.event;
}
public showContextMenu(delegate: IContextMenuDelegate): void {
showContextMenu(delegate: IContextMenuDelegate): void {
delegate.getActions().then(actions => {
if (!actions.length) {
return TPromise.as(null);
}
return TPromise.timeout(0).then(() => { // https://github.com/Microsoft/vscode/issues/3638
const menu = this.createMenu(delegate, actions);
const onHide = once(() => {
if (delegate.onHide) {
delegate.onHide(undefined);
}
this._onDidContextMenu.fire();
});
const menu = this.createMenu(delegate, actions, onHide);
const anchor = delegate.getAnchor();
let x: number, y: number;
@@ -61,16 +69,18 @@ export class ContextMenuService implements IContextMenuService {
x *= zoom;
y *= zoom;
menu.popup(remote.getCurrentWindow(), { x: Math.floor(x), y: Math.floor(y), positioningItem: delegate.autoSelectFirstItem ? 0 : void 0 });
this._onDidContextMenu.fire();
if (delegate.onHide) {
delegate.onHide(undefined);
}
menu.popup({
window: remote.getCurrentWindow(),
x: Math.floor(x),
y: Math.floor(y),
positioningItem: delegate.autoSelectFirstItem ? 0 : void 0,
callback: () => onHide()
});
});
});
}
private createMenu(delegate: IContextMenuDelegate, entries: (IAction | ContextSubMenu)[]): Electron.Menu {
private createMenu(delegate: IContextMenuDelegate, entries: (IAction | ContextSubMenu)[], onHide: () => void): Electron.Menu {
const menu = new remote.Menu();
const actionRunner = delegate.actionRunner || new ActionRunner();
@@ -79,7 +89,7 @@ export class ContextMenuService implements IContextMenuService {
menu.append(new remote.MenuItem({ type: 'separator' }));
} else if (e instanceof ContextSubMenu) {
const submenu = new remote.MenuItem({
submenu: this.createMenu(delegate, e.entries),
submenu: this.createMenu(delegate, e.entries, onHide),
label: unmnemonicLabel(e.label)
});
@@ -91,6 +101,13 @@ export class ContextMenuService implements IContextMenuService {
type: !!e.checked ? 'checkbox' : !!e.radio ? 'radio' : void 0,
enabled: !!e.enabled,
click: (menuItem, win, event) => {
// To preserve pre-electron-2.x behaviour, we first trigger
// the onHide callback and then the action.
// Fixes https://github.com/Microsoft/vscode/issues/45601
onHide();
// Run action which will close the menu
this.runAction(actionRunner, e, delegate, event);
}
};

View File

@@ -36,8 +36,9 @@ configurationRegistry.registerConfiguration({
'properties': {
'telemetry.enableCrashReporter': {
'type': 'boolean',
'description': nls.localize('telemetry.enableCrashReporting', "Enable crash reports to be sent to Microsoft.\nThis option requires restart to take effect."),
'default': true
'description': nls.localize('telemetry.enableCrashReporting', "Enable crash reports to be sent to a Microsoft online service. \nThis option requires restart to take effect."),
'default': true,
'tags': ['usesOnlineServices']
}
}
});
@@ -54,7 +55,7 @@ export const NullCrashReporterService: ICrashReporterService = {
export class CrashReporterService implements ICrashReporterService {
public _serviceBrand: any;
_serviceBrand: any;
private options: Electron.CrashReporterStartOptions;
private isEnabled: boolean;
@@ -114,7 +115,7 @@ export class CrashReporterService implements ICrashReporterService {
return submitURL;
}
public getChildProcessStartOptions(name: string): Electron.CrashReporterStartOptions {
getChildProcessStartOptions(name: string): Electron.CrashReporterStartOptions {
// Experimental crash reporting support for child processes on Mac only for now
if (this.isEnabled && isMacintosh) {

View File

@@ -9,7 +9,7 @@ import URI from 'vs/base/common/uri';
import { Event } from 'vs/base/common/event';
import { ColorIdentifier } from 'vs/platform/theme/common/colorRegistry';
import { IDisposable } from 'vs/base/common/lifecycle';
import { TPromise } from 'vs/base/common/winjs.base';
import { CancellationToken } from 'vs/base/common/cancellation';
export const IDecorationsService = createDecorator<IDecorationsService>('IFileDecorationsService');
@@ -32,7 +32,7 @@ export interface IDecoration {
export interface IDecorationsProvider {
readonly label: string;
readonly onDidChange: Event<URI[]>;
provideDecorations(uri: URI): IDecorationData | TPromise<IDecorationData>;
provideDecorations(uri: URI, token: CancellationToken): IDecorationData | Thenable<IDecorationData>;
}
export interface IResourceDecorationChangeEvent {

View File

@@ -8,17 +8,17 @@ import URI from 'vs/base/common/uri';
import { Event, Emitter, debounceEvent, anyEvent } from 'vs/base/common/event';
import { IDecorationsService, IDecoration, IResourceDecorationChangeEvent, IDecorationsProvider, IDecorationData } from './decorations';
import { TernarySearchTree } from 'vs/base/common/map';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { IDisposable, dispose, 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';
import { IThemeService, ITheme } from 'vs/platform/theme/common/themeService';
import { IdGenerator } from 'vs/base/common/idGenerator';
import { IIterator } from 'vs/base/common/iterator';
import { Iterator } from 'vs/base/common/iterator';
import { isFalsyOrWhitespace } from 'vs/base/common/strings';
import { localize } from 'vs/nls';
import { TPromise } from 'vs/base/common/winjs.base';
import { isPromiseCanceledError } from 'vs/base/common/errors';
import { CancellationTokenSource } from 'vs/base/common/cancellation';
class DecorationRule {
@@ -174,13 +174,13 @@ class DecorationStyles {
});
}
cleanUp(iter: IIterator<DecorationProviderWrapper>): void {
cleanUp(iter: Iterator<DecorationProviderWrapper>): void {
// remove every rule for which no more
// decoration (data) is kept. this isn't cheap
let usedDecorations = new Set<string>();
for (let e = iter.next(); !e.done; e = iter.next()) {
e.value.data.forEach((value, key) => {
if (!isThenable<any>(value) && value) {
if (value && !(value instanceof DecorationDataRequest)) {
usedDecorations.add(DecorationRule.keyOf(value));
}
});
@@ -229,9 +229,16 @@ class FileDecorationChangeEvent implements IResourceDecorationChangeEvent {
}
}
class DecorationDataRequest {
constructor(
readonly source: CancellationTokenSource,
readonly thenable: Thenable<void>,
) { }
}
class DecorationProviderWrapper {
readonly data = TernarySearchTree.forPaths<TPromise<void> | IDecorationData>();
readonly data = TernarySearchTree.forPaths<DecorationDataRequest | IDecorationData>();
private readonly _dispoable: IDisposable;
constructor(
@@ -270,30 +277,25 @@ class DecorationProviderWrapper {
const key = uri.toString();
let item = this.data.get(key);
if (isThenable<void>(item)) {
// pending -> still waiting
return;
}
if (item === undefined) {
// unknown -> trigger request
item = this._fetchData(uri);
}
if (item) {
// found something
if (item && !(item instanceof DecorationDataRequest)) {
// found something (which isn't pending anymore)
callback(item, false);
}
if (includeChildren) {
// (resolved) children
const childTree = this.data.findSuperstr(key);
if (childTree) {
childTree.forEach(value => {
if (value && !isThenable<void>(value)) {
callback(value, true);
const iter = this.data.findSuperstr(key);
if (iter) {
for (let item = iter.next(); !item.done; item = iter.next()) {
if (item.value && !(item.value instanceof DecorationDataRequest)) {
callback(item.value, true);
}
});
}
}
}
}
@@ -302,27 +304,28 @@ class DecorationProviderWrapper {
// check for pending request and cancel it
const pendingRequest = this.data.get(uri.toString());
if (TPromise.is(pendingRequest)) {
pendingRequest.cancel();
if (pendingRequest instanceof DecorationDataRequest) {
pendingRequest.source.cancel();
this.data.delete(uri.toString());
}
const dataOrThenable = this._provider.provideDecorations(uri);
const source = new CancellationTokenSource();
const dataOrThenable = this._provider.provideDecorations(uri, source.token);
if (!isThenable(dataOrThenable)) {
// sync -> we have a result now
return this._keepItem(uri, dataOrThenable);
} else {
// async -> we have a result soon
const request = TPromise.wrap(dataOrThenable).then(data => {
const request = new DecorationDataRequest(source, Promise.resolve(dataOrThenable).then(data => {
if (this.data.get(uri.toString()) === request) {
this._keepItem(uri, data);
}
}, err => {
}).catch(err => {
if (!isPromiseCanceledError(err) && this.data.get(uri.toString()) === request) {
this.data.delete(uri.toString());
}
});
}));
// {{ SQL CARBON EDIT }} - Add type assertion to fix build break
this.data.set(uri.toString(), <any>request);
@@ -400,15 +403,13 @@ export class FileDecorationsService implements IDecorationsService {
affectsResource() { return true; }
});
return {
dispose: () => {
// fire event that says 'yes' for any resource
// known to this provider. then dispose and remove it.
remove();
this._onDidChangeDecorations.fire({ affectsResource: uri => wrapper.knowsAbout(uri) });
wrapper.dispose();
}
};
return toDisposable(() => {
// fire event that says 'yes' for any resource
// known to this provider. then dispose and remove it.
remove();
this._onDidChangeDecorations.fire({ affectsResource: uri => wrapper.knowsAbout(uri) });
wrapper.dispose();
});
}
getDecoration(uri: URI, includeChildren: boolean, overwrite?: IDecorationData): IDecoration {

View File

@@ -12,6 +12,7 @@ import URI from 'vs/base/common/uri';
import { Event, toPromise, Emitter } from 'vs/base/common/event';
import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService';
import { TPromise } from 'vs/base/common/winjs.base';
import { CancellationToken } from 'vs/base/common/cancellation';
suite('DecorationsService', function () {
@@ -34,7 +35,7 @@ suite('DecorationsService', function () {
readonly onDidChange: Event<URI[]> = Event.None;
provideDecorations(uri: URI) {
callCounter += 1;
return new TPromise<IDecorationData>(resolve => {
return new Promise<IDecorationData>(resolve => {
setTimeout(() => resolve({
color: 'someBlue',
tooltip: 'T'
@@ -174,6 +175,7 @@ suite('DecorationsService', function () {
test('Decorations not showing up for second root folder #48502', async function () {
let cancelCount = 0;
let winjsCancelCount = 0;
let callCount = 0;
let provider = new class implements IDecorationsProvider {
@@ -183,14 +185,19 @@ suite('DecorationsService', function () {
label: string = 'foo';
provideDecorations(uri): TPromise<IDecorationData> {
provideDecorations(uri: URI, token: CancellationToken): TPromise<IDecorationData> {
token.onCancellationRequested(() => {
cancelCount += 1;
});
return new TPromise(resolve => {
callCount += 1;
setTimeout(() => {
resolve({ letter: 'foo' });
}, 10);
}, () => {
cancelCount += 1;
winjsCancelCount += 1;
});
}
};
@@ -204,8 +211,37 @@ suite('DecorationsService', function () {
service.getDecoration(uri, false);
assert.equal(cancelCount, 1);
assert.equal(winjsCancelCount, 0);
assert.equal(callCount, 2);
reg.dispose();
});
test('Decorations not bubbling... #48745', function () {
let resolve: Function;
let reg = service.registerDecorationsProvider({
label: 'Test',
onDidChange: Event.None,
provideDecorations(uri: URI) {
if (uri.path.match(/hello$/)) {
return { tooltip: 'FOO', weight: 17, bubble: true };
} else {
return new Promise<IDecorationData>(_resolve => resolve = _resolve);
}
}
});
let data1 = service.getDecoration(URI.parse('a:b/'), true);
assert.ok(!data1);
let data2 = service.getDecoration(URI.parse('a:b/c.hello'), false);
assert.ok(data2.tooltip);
let data3 = service.getDecoration(URI.parse('a:b/'), true);
assert.ok(data3);
reg.dispose();
});
});

View File

@@ -13,6 +13,7 @@ import { isLinux, isWindows } from 'vs/base/common/platform';
import { IWindowService } from 'vs/platform/windows/common/windows';
import { mnemonicButtonLabel } from 'vs/base/common/labels';
import { IDialogService, IConfirmation, IConfirmationResult, IDialogOptions } from 'vs/platform/dialogs/common/dialogs';
import { ILogService } from 'vs/platform/log/common/log';
interface IMassagedMessageBoxOptions {
@@ -31,14 +32,16 @@ interface IMassagedMessageBoxOptions {
export class DialogService implements IDialogService {
public _serviceBrand: any;
_serviceBrand: any;
constructor(
@IWindowService private windowService: IWindowService
) {
}
@IWindowService private windowService: IWindowService,
@ILogService private logService: ILogService
) { }
confirm(confirmation: IConfirmation): TPromise<IConfirmationResult> {
this.logService.trace('DialogService#confirm', confirmation.message);
public confirm(confirmation: IConfirmation): TPromise<IConfirmationResult> {
const { options, buttonIndexMap } = this.massageMessageBoxOptions(this.getConfirmOptions(confirmation));
return this.windowService.showMessageBox(options).then(result => {
@@ -86,7 +89,9 @@ export class DialogService implements IDialogService {
return opts;
}
public show(severity: Severity, message: string, buttons: string[], dialogOptions?: IDialogOptions): TPromise<number> {
show(severity: Severity, message: string, buttons: string[], dialogOptions?: IDialogOptions): TPromise<number> {
this.logService.trace('DialogService#show', message);
const { options, buttonIndexMap } = this.massageMessageBoxOptions({
message,
buttons,

View File

@@ -0,0 +1,617 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { IResourceInput, ITextEditorOptions, IEditorOptions } from 'vs/platform/editor/common/editor';
import { IEditorInput, IEditor, GroupIdentifier, IFileEditorInput, IUntitledResourceInput, IResourceDiffInput, IResourceSideBySideInput, IEditorInputFactoryRegistry, Extensions as EditorExtensions, IFileInputFactory, EditorInput, SideBySideEditorInput, IEditorInputWithOptions, isEditorInputWithOptions, EditorOptions, TextEditorOptions, IEditorIdentifier, IEditorCloseEvent, ITextEditor, ITextDiffEditor, ITextSideBySideEditor, toResource } from 'vs/workbench/common/editor';
import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput';
import { DataUriEditorInput } from 'vs/workbench/common/editor/dataUriEditorInput';
import { Registry } from 'vs/platform/registry/common/platform';
import { ResourceMap } from 'vs/base/common/map';
import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService';
import { IFileService } from 'vs/platform/files/common/files';
import { Schemas } from 'vs/base/common/network';
import { Event, once, Emitter } from 'vs/base/common/event';
import URI from 'vs/base/common/uri';
import { basename } from 'vs/base/common/paths';
import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput';
import { localize } from 'vs/nls';
import { TPromise } from 'vs/base/common/winjs.base';
import { IEditorGroupsService, IEditorGroup, GroupsOrder, IEditorReplacement, GroupChangeKind, preferredSideBySideGroupDirection } from 'vs/workbench/services/group/common/editorGroupsService';
import { IResourceEditor, ACTIVE_GROUP_TYPE, SIDE_GROUP_TYPE, SIDE_GROUP, IResourceEditorReplacement, IOpenEditorOverrideHandler } 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 { coalesce } from 'vs/base/common/arrays';
import { isCodeEditor, isDiffEditor, ICodeEditor, IDiffEditor } from 'vs/editor/browser/editorBrowser';
import { IEditorGroupView, IEditorOpeningEvent, EditorGroupsServiceImpl, EditorServiceImpl } from 'vs/workbench/browser/parts/editor/editor';
import { IUriDisplayService } from 'vs/platform/uriDisplay/common/uriDisplay';
type ICachedEditorInput = ResourceEditorInput | IFileEditorInput | DataUriEditorInput;
export class EditorService extends Disposable implements EditorServiceImpl {
_serviceBrand: any;
private static CACHE: ResourceMap<ICachedEditorInput> = new ResourceMap<ICachedEditorInput>();
//#region events
private _onDidActiveEditorChange: Emitter<void> = this._register(new Emitter<void>());
get onDidActiveEditorChange(): Event<void> { return this._onDidActiveEditorChange.event; }
private _onDidVisibleEditorsChange: Emitter<void> = this._register(new Emitter<void>());
get onDidVisibleEditorsChange(): Event<void> { return this._onDidVisibleEditorsChange.event; }
private _onDidCloseEditor: Emitter<IEditorCloseEvent> = this._register(new Emitter<IEditorCloseEvent>());
get onDidCloseEditor(): Event<IEditorCloseEvent> { return this._onDidCloseEditor.event; }
private _onDidOpenEditorFail: Emitter<IEditorIdentifier> = this._register(new Emitter<IEditorIdentifier>());
get onDidOpenEditorFail(): Event<IEditorIdentifier> { return this._onDidOpenEditorFail.event; }
//#endregion
private fileInputFactory: IFileInputFactory;
private openEditorHandlers: IOpenEditorOverrideHandler[] = [];
private lastActiveEditor: IEditorInput;
private lastActiveGroupId: GroupIdentifier;
constructor(
@IEditorGroupsService private editorGroupService: EditorGroupsServiceImpl,
@IUntitledEditorService private untitledEditorService: IUntitledEditorService,
@IInstantiationService private instantiationService: IInstantiationService,
@IUriDisplayService private uriDisplayService: IUriDisplayService,
@IFileService private fileService: IFileService,
@IConfigurationService private configurationService: IConfigurationService
) {
super();
this.fileInputFactory = Registry.as<IEditorInputFactoryRegistry>(EditorExtensions.EditorInputFactories).getFileInputFactory();
this.registerListeners();
}
private registerListeners(): void {
this.editorGroupService.whenRestored.then(() => this.onEditorsRestored());
this.editorGroupService.onDidActiveGroupChange(group => this.handleActiveEditorChange(group));
this.editorGroupService.onDidAddGroup(group => this.registerGroupListeners(group as IEditorGroupView));
}
private onEditorsRestored(): void {
// Register listeners to each opened group
this.editorGroupService.groups.forEach(group => this.registerGroupListeners(group as IEditorGroupView));
// Fire initial set of editor events if there is an active editor
if (this.activeEditor) {
this.doEmitActiveEditorChangeEvent();
this._onDidVisibleEditorsChange.fire();
}
}
private handleActiveEditorChange(group: IEditorGroup): void {
if (group !== this.editorGroupService.activeGroup) {
return; // ignore if not the active group
}
if (!this.lastActiveEditor && !group.activeEditor) {
return; // ignore if we still have no active editor
}
if (this.lastActiveGroupId === group.id && this.lastActiveEditor === group.activeEditor) {
return; // ignore if the editor actually did not change
}
this.doEmitActiveEditorChangeEvent();
}
private doEmitActiveEditorChangeEvent(): void {
const activeGroup = this.editorGroupService.activeGroup;
this.lastActiveGroupId = activeGroup.id;
this.lastActiveEditor = activeGroup.activeEditor;
this._onDidActiveEditorChange.fire();
}
private registerGroupListeners(group: IEditorGroupView): void {
const groupDisposeables: IDisposable[] = [];
groupDisposeables.push(group.onDidGroupChange(e => {
if (e.kind === GroupChangeKind.EDITOR_ACTIVE) {
this.handleActiveEditorChange(group);
this._onDidVisibleEditorsChange.fire();
}
}));
groupDisposeables.push(group.onDidCloseEditor(event => {
this._onDidCloseEditor.fire(event);
}));
groupDisposeables.push(group.onWillOpenEditor(event => {
this.onGroupWillOpenEditor(group, event);
}));
groupDisposeables.push(group.onDidOpenEditorFail(editor => {
this._onDidOpenEditorFail.fire({ editor, groupId: group.id });
}));
once(group.onWillDispose)(() => {
dispose(groupDisposeables);
});
}
private onGroupWillOpenEditor(group: IEditorGroup, event: IEditorOpeningEvent): void {
for (let i = 0; i < this.openEditorHandlers.length; i++) {
const handler = this.openEditorHandlers[i];
const result = handler(event.editor, event.options, group);
if (result && result.override) {
event.prevent((() => result.override));
break;
}
}
}
get activeControl(): IEditor {
const activeGroup = this.editorGroupService.activeGroup;
return activeGroup ? activeGroup.activeControl : void 0;
}
get activeTextEditorWidget(): ICodeEditor | IDiffEditor {
const activeControl = this.activeControl;
if (activeControl) {
const activeControlWidget = activeControl.getControl();
if (isCodeEditor(activeControlWidget) || isDiffEditor(activeControlWidget)) {
return activeControlWidget;
}
}
return void 0;
}
get editors(): IEditorInput[] {
const editors: IEditorInput[] = [];
this.editorGroupService.groups.forEach(group => {
editors.push(...group.editors);
});
return editors;
}
get activeEditor(): IEditorInput {
const activeGroup = this.editorGroupService.activeGroup;
return activeGroup ? activeGroup.activeEditor : void 0;
}
get visibleControls(): IEditor[] {
return coalesce(this.editorGroupService.groups.map(group => group.activeControl));
}
get visibleTextEditorWidgets(): (ICodeEditor | IDiffEditor)[] {
return this.visibleControls.map(control => control.getControl() as ICodeEditor | IDiffEditor).filter(widget => isCodeEditor(widget) || isDiffEditor(widget));
}
get visibleEditors(): IEditorInput[] {
return coalesce(this.editorGroupService.groups.map(group => group.activeEditor));
}
//#region preventOpenEditor()
overrideOpenEditor(handler: IOpenEditorOverrideHandler): IDisposable {
this.openEditorHandlers.push(handler);
return toDisposable(() => {
const index = this.openEditorHandlers.indexOf(handler);
if (index >= 0) {
this.openEditorHandlers.splice(index, 1);
}
});
}
//#endregion
//#region openEditor()
openEditor(editor: IEditorInput, options?: IEditorOptions | ITextEditorOptions, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): TPromise<IEditor>;
openEditor(editor: IResourceInput | IUntitledResourceInput, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): TPromise<ITextEditor>;
openEditor(editor: IResourceDiffInput, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): TPromise<ITextDiffEditor>;
openEditor(editor: IResourceSideBySideInput, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): TPromise<ITextSideBySideEditor>;
openEditor(editor: IEditorInput | IResourceEditor, optionsOrGroup?: IEditorOptions | ITextEditorOptions | IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE, group?: GroupIdentifier): TPromise<IEditor> {
// Typed Editor Support
if (editor instanceof EditorInput) {
const editorOptions = this.toOptions(optionsOrGroup as IEditorOptions);
const targetGroup = this.findTargetGroup(editor, editorOptions, group);
return this.doOpenEditor(targetGroup, editor, editorOptions);
}
// Untyped Text Editor Support
const textInput = <IResourceEditor>editor;
const typedInput = this.createInput(textInput);
if (typedInput) {
const editorOptions = TextEditorOptions.from(textInput);
const targetGroup = this.findTargetGroup(typedInput, editorOptions, optionsOrGroup as IEditorGroup | GroupIdentifier);
return this.doOpenEditor(targetGroup, typedInput, editorOptions);
}
return TPromise.wrap<IEditor>(null);
}
protected doOpenEditor(group: IEditorGroup, editor: IEditorInput, options?: IEditorOptions): TPromise<IEditor> {
return group.openEditor(editor, options).then(() => group.activeControl);
}
private findTargetGroup(input: IEditorInput, options?: IEditorOptions, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): IEditorGroup {
let targetGroup: IEditorGroup;
// Group: Instance of Group
if (group && typeof group !== 'number') {
return group;
}
// Group: Side by Side
if (group === SIDE_GROUP) {
targetGroup = this.findSideBySideGroup();
}
// Group: Specific Group
else if (typeof group === 'number' && group >= 0) {
targetGroup = this.editorGroupService.getGroup(group);
}
// Group: Unspecified without a specific index to open
else if (!options || typeof options.index !== 'number') {
const groupsByLastActive = this.editorGroupService.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE);
// Respect option to reveal an editor if it is already visible in any group
if (options && options.revealIfVisible) {
for (let i = 0; i < groupsByLastActive.length; i++) {
const group = groupsByLastActive[i];
if (input.matches(group.activeEditor)) {
targetGroup = group;
break;
}
}
}
// Respect option to reveal an editor if it is open (not necessarily visible)
if ((options && options.revealIfOpened) || this.configurationService.getValue<boolean>('workbench.editor.revealIfOpen')) {
for (let i = 0; i < groupsByLastActive.length; i++) {
const group = groupsByLastActive[i];
if (group.isOpened(input)) {
targetGroup = group;
break;
}
}
}
}
// Fallback to active group if target not valid
if (!targetGroup) {
targetGroup = this.editorGroupService.activeGroup;
}
return targetGroup;
}
private findSideBySideGroup(): IEditorGroup {
const direction = preferredSideBySideGroupDirection(this.configurationService);
let neighbourGroup = this.editorGroupService.findGroup({ direction });
if (!neighbourGroup) {
neighbourGroup = this.editorGroupService.addGroup(this.editorGroupService.activeGroup, direction);
}
return neighbourGroup;
}
private toOptions(options?: IEditorOptions | EditorOptions): EditorOptions {
if (!options || options instanceof EditorOptions) {
return options as EditorOptions;
}
const textOptions: ITextEditorOptions = options;
if (!!textOptions.selection) {
return TextEditorOptions.create(options);
}
return EditorOptions.create(options);
}
//#endregion
//#region openEditors()
openEditors(editors: IEditorInputWithOptions[], group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): TPromise<IEditor[]>;
openEditors(editors: IResourceEditor[], group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): TPromise<IEditor[]>;
openEditors(editors: (IEditorInputWithOptions | IResourceEditor)[], group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): TPromise<IEditor[]> {
// Convert to typed editors and options
const typedEditors: IEditorInputWithOptions[] = [];
editors.forEach(editor => {
if (isEditorInputWithOptions(editor)) {
typedEditors.push(editor);
} else {
typedEditors.push({ editor: this.createInput(editor), options: TextEditorOptions.from(editor) });
}
});
// Find target groups to open
const mapGroupToEditors = new Map<IEditorGroup, IEditorInputWithOptions[]>();
if (group === SIDE_GROUP) {
mapGroupToEditors.set(this.findSideBySideGroup(), typedEditors);
} else {
typedEditors.forEach(typedEditor => {
const targetGroup = this.findTargetGroup(typedEditor.editor, typedEditor.options, group);
let targetGroupEditors = mapGroupToEditors.get(targetGroup);
if (!targetGroupEditors) {
targetGroupEditors = [];
mapGroupToEditors.set(targetGroup, targetGroupEditors);
}
targetGroupEditors.push(typedEditor);
});
}
// Open in targets
const result: TPromise<IEditor>[] = [];
mapGroupToEditors.forEach((editorsWithOptions, group) => {
result.push((group.openEditors(editorsWithOptions)).then(() => group.activeControl));
});
return TPromise.join(result);
}
//#endregion
//#region isOpen()
isOpen(editor: IEditorInput | IResourceInput | IUntitledResourceInput, group?: IEditorGroup | GroupIdentifier): boolean {
let groups: IEditorGroup[] = [];
if (typeof group === 'number') {
groups.push(this.editorGroupService.getGroup(group));
} else if (group) {
groups.push(group);
} else {
groups = [...this.editorGroupService.groups];
}
return groups.some(group => {
if (editor instanceof EditorInput) {
return group.isOpened(editor);
}
const resourceInput = editor as IResourceInput | IUntitledResourceInput;
if (!resourceInput.resource) {
return false;
}
return group.editors.some(editorInGroup => {
const resource = toResource(editorInGroup, { supportSideBySide: true });
return resource && resource.toString() === resourceInput.resource.toString();
});
});
}
//#endregion
//#region replaceEditors()
replaceEditors(editors: IResourceEditorReplacement[], group: IEditorGroup | GroupIdentifier): TPromise<void>;
replaceEditors(editors: IEditorReplacement[], group: IEditorGroup | GroupIdentifier): TPromise<void>;
replaceEditors(editors: (IEditorReplacement | IResourceEditorReplacement)[], group: IEditorGroup | GroupIdentifier): TPromise<void> {
const typedEditors: IEditorReplacement[] = [];
editors.forEach(replaceEditorArg => {
if (replaceEditorArg.editor instanceof EditorInput) {
typedEditors.push(replaceEditorArg as IEditorReplacement);
} else {
const editor = replaceEditorArg.editor as IResourceEditor;
const typedEditor = this.createInput(editor);
const replacementEditor = this.createInput(replaceEditorArg.replacement as IResourceEditor);
typedEditors.push({
editor: typedEditor,
replacement: replacementEditor,
options: this.toOptions(editor.options)
});
}
});
const targetGroup = typeof group === 'number' ? this.editorGroupService.getGroup(group) : group;
return targetGroup.replaceEditors(typedEditors);
}
//#endregion
//#region invokeWithinEditorContext()
invokeWithinEditorContext<T>(fn: (accessor: ServicesAccessor) => T): T {
const activeTextEditorWidget = this.activeTextEditorWidget;
if (isCodeEditor(activeTextEditorWidget)) {
return activeTextEditorWidget.invokeWithinContext(fn);
}
const activeGroup = this.editorGroupService.activeGroup;
if (activeGroup) {
return activeGroup.invokeWithinContext(fn);
}
return this.instantiationService.invokeFunction(fn);
}
//#endregion
//#region createInput()
createInput(input: IEditorInputWithOptions | IEditorInput | IResourceEditor, options?: { forceFileInput: boolean }): EditorInput {
// Typed Editor Input Support (EditorInput)
if (input instanceof EditorInput) {
return input;
}
// Typed Editor Input Support (IEditorInputWithOptions)
const editorInputWithOptions = input as IEditorInputWithOptions;
if (editorInputWithOptions.editor instanceof EditorInput) {
return editorInputWithOptions.editor;
}
// Side by Side Support
const resourceSideBySideInput = <IResourceSideBySideInput>input;
if (resourceSideBySideInput.masterResource && resourceSideBySideInput.detailResource) {
const masterInput = this.createInput({ resource: resourceSideBySideInput.masterResource }, options);
const detailInput = this.createInput({ resource: resourceSideBySideInput.detailResource }, options);
return new SideBySideEditorInput(
resourceSideBySideInput.label || masterInput.getName(),
typeof resourceSideBySideInput.description === 'string' ? resourceSideBySideInput.description : masterInput.getDescription(),
detailInput,
masterInput
);
}
// Diff Editor Support
const resourceDiffInput = <IResourceDiffInput>input;
if (resourceDiffInput.leftResource && resourceDiffInput.rightResource) {
const leftInput = this.createInput({ resource: resourceDiffInput.leftResource }, options);
const rightInput = this.createInput({ resource: resourceDiffInput.rightResource }, options);
const label = resourceDiffInput.label || localize('compareLabels', "{0} ↔ {1}", this.toDiffLabel(leftInput), this.toDiffLabel(rightInput));
return new DiffEditorInput(label, resourceDiffInput.description, leftInput, rightInput);
}
// Untitled file support
const untitledInput = <IUntitledResourceInput>input;
if (!untitledInput.resource || typeof untitledInput.filePath === 'string' || (untitledInput.resource instanceof URI && untitledInput.resource.scheme === Schemas.untitled)) {
// {{SQL CARBON EDIT}}
return this.untitledEditorService.createOrGet(
untitledInput.filePath ? URI.file(untitledInput.filePath) : untitledInput.resource,
'sql',
untitledInput.contents,
untitledInput.encoding
);
}
// Resource Editor Support
const resourceInput = <IResourceInput>input;
if (resourceInput.resource instanceof URI) {
let label = resourceInput.label;
if (!label && resourceInput.resource.scheme !== Schemas.data) {
label = basename(resourceInput.resource.fsPath); // derive the label from the path (but not for data URIs)
}
return this.createOrGet(resourceInput.resource, this.instantiationService, label, resourceInput.description, resourceInput.encoding, options && options.forceFileInput) as EditorInput;
}
return null;
}
private createOrGet(resource: URI, instantiationService: IInstantiationService, label: string, description: string, encoding?: string, forceFileInput?: boolean): ICachedEditorInput {
if (EditorService.CACHE.has(resource)) {
const input = EditorService.CACHE.get(resource);
if (input instanceof ResourceEditorInput) {
input.setName(label);
input.setDescription(description);
} else if (!(input instanceof DataUriEditorInput)) {
input.setPreferredEncoding(encoding);
}
return input;
}
let input: ICachedEditorInput;
// File
if (this.fileService.canHandleResource(resource) || forceFileInput /* fix for https://github.com/Microsoft/vscode/issues/48275 */) {
input = this.fileInputFactory.createFileInput(resource, encoding, instantiationService);
}
// Data URI
else if (resource.scheme === Schemas.data) {
input = instantiationService.createInstance(DataUriEditorInput, label, description, resource);
}
// Resource
else {
input = instantiationService.createInstance(ResourceEditorInput, label, description, resource);
}
EditorService.CACHE.set(resource, input);
once(input.onDispose)(() => {
EditorService.CACHE.delete(resource);
});
return input;
}
private toDiffLabel(input: EditorInput): string {
const res = input.getResource();
// Do not try to extract any paths from simple untitled editors
if (res.scheme === Schemas.untitled && !this.untitledEditorService.hasAssociatedFilePath(res)) {
return input.getName();
}
// Otherwise: for diff labels prefer to see the path as part of the label
return this.uriDisplayService.getLabel(res, true);
}
//#endregion
}
export interface IEditorOpenHandler {
(group: IEditorGroup, editor: IEditorInput, options?: IEditorOptions | ITextEditorOptions): TPromise<IEditor>;
}
/**
* The delegating workbench editor service can be used to override the behaviour of the openEditor()
* method by providing a IEditorOpenHandler.
*/
export class DelegatingEditorService extends EditorService {
private editorOpenHandler: IEditorOpenHandler;
constructor(
@IEditorGroupsService editorGroupService: EditorGroupsServiceImpl,
@IUntitledEditorService untitledEditorService: IUntitledEditorService,
@IInstantiationService instantiationService: IInstantiationService,
@IUriDisplayService uriDisplayService: IUriDisplayService,
@IFileService fileService: IFileService,
@IConfigurationService configurationService: IConfigurationService
) {
super(
editorGroupService,
untitledEditorService,
instantiationService,
uriDisplayService,
fileService,
configurationService
);
}
setEditorOpenHandler(handler: IEditorOpenHandler): void {
this.editorOpenHandler = handler;
}
protected doOpenEditor(group: IEditorGroup, editor: IEditorInput, options?: IEditorOptions): TPromise<IEditor> {
const handleOpen = this.editorOpenHandler ? this.editorOpenHandler(group, editor, options) : TPromise.as(void 0);
return handleOpen.then(control => {
if (control) {
return TPromise.as<IEditor>(control); // the opening was handled, so return early
}
return super.doOpenEditor(group, editor, options);
});
}
}

View File

@@ -5,449 +5,166 @@
'use strict';
import { createDecorator, ServiceIdentifier, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { IResourceInput, IEditorOptions, ITextEditorOptions } from 'vs/platform/editor/common/editor';
import { IEditorInput, IEditor, GroupIdentifier, IEditorInputWithOptions, IUntitledResourceInput, IResourceDiffInput, IResourceSideBySideInput, ITextEditor, ITextDiffEditor, ITextSideBySideEditor } from 'vs/workbench/common/editor';
import { Event } from 'vs/base/common/event';
import { IEditor as ICodeEditor } from 'vs/editor/common/editorCommon';
import { IEditorGroup, IEditorReplacement } from 'vs/workbench/services/group/common/editorGroupsService';
import { TPromise } from 'vs/base/common/winjs.base';
import { createDecorator, ServiceIdentifier, IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IEditorService, IEditor, IEditorInput, IEditorOptions, ITextEditorOptions, Position, Direction, IResourceInput, IResourceDiffInput, IResourceSideBySideInput, IUntitledResourceInput } from 'vs/platform/editor/common/editor';
import URI from 'vs/base/common/uri';
import { Registry } from 'vs/platform/registry/common/platform';
import { basename } from 'vs/base/common/paths';
import { EditorInput, EditorOptions, TextEditorOptions, Extensions as EditorExtensions, SideBySideEditorInput, IFileEditorInput, IFileInputFactory, IEditorInputFactoryRegistry } from 'vs/workbench/common/editor';
import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput';
import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService';
import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import * as nls from 'vs/nls';
import { getPathLabel } from 'vs/base/common/labels';
import { ResourceMap } from 'vs/base/common/map';
import { once } from 'vs/base/common/event';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IFileService } from 'vs/platform/files/common/files';
import { DataUriEditorInput } from 'vs/workbench/common/editor/dataUriEditorInput';
import { Schemas } from 'vs/base/common/network';
import { IDisposable } from 'vs/base/common/lifecycle';
export const IWorkbenchEditorService = createDecorator<IWorkbenchEditorService>('editorService');
export const IEditorService = createDecorator<IEditorService>('editorService');
export type IResourceInputType = IResourceInput | IUntitledResourceInput | IResourceDiffInput | IResourceSideBySideInput;
export type IResourceEditor = IResourceInput | IUntitledResourceInput | IResourceDiffInput | IResourceSideBySideInput;
export type ICloseEditorsFilter = { except?: IEditorInput, direction?: Direction, savedOnly?: boolean };
export interface IResourceEditorReplacement {
editor: IResourceEditor;
replacement: IResourceEditor;
}
/**
* The editor service allows to open editors and work on the active
* editor input and models.
*/
export interface IWorkbenchEditorService extends IEditorService {
export const ACTIVE_GROUP = -1;
export type ACTIVE_GROUP_TYPE = typeof ACTIVE_GROUP;
export const SIDE_GROUP = -2;
export type SIDE_GROUP_TYPE = typeof SIDE_GROUP;
export interface IOpenEditorOverrideHandler {
(editor: IEditorInput, options: IEditorOptions | ITextEditorOptions, group: IEditorGroup): IOpenEditorOverride;
}
export interface IOpenEditorOverride {
/**
* If defined, will prevent the opening of an editor and replace the resulting
* promise with the provided promise for the openEditor() call.
*/
override?: TPromise<any>;
}
export interface IEditorService {
_serviceBrand: ServiceIdentifier<any>;
/**
* Returns the currently active editor or null if none.
* Emitted when the currently active editor changes.
*
* @see `IEditorService.activeEditor`
*/
getActiveEditor(): IEditor;
readonly onDidActiveEditorChange: Event<void>;
/**
* Returns the currently active editor input or null if none.
* Emitted when any of the current visible editors changes.
*
* @see `IEditorService.visibleEditors`
*/
getActiveEditorInput(): IEditorInput;
readonly onDidVisibleEditorsChange: Event<void>;
/**
* Returns an array of visible editors.
* The currently active editor or `undefined` if none. An editor is active when it is
* located in the currently active editor group. It will be `undefined` if the active
* editor group has no editors open.
*/
getVisibleEditors(): IEditor[];
readonly activeEditor: IEditorInput;
/**
* Opens an Editor on the given input with the provided options at the given position. If sideBySide parameter
* is provided, causes the editor service to decide in what position to open the input.
* The currently active editor control or `undefined` if none. The editor control is
* the workbench container for editors of any kind.
*
* @see `IEditorService.activeEditor`
*/
openEditor(input: IEditorInput, options?: IEditorOptions | ITextEditorOptions, position?: Position): TPromise<IEditor>;
openEditor(input: IEditorInput, options?: IEditorOptions | ITextEditorOptions, sideBySide?: boolean): TPromise<IEditor>;
readonly activeControl: IEditor;
/**
* Specific overload to open an instance of IResourceInput, IResourceDiffInput or IResourceSideBySideInput.
* The currently active text editor widget or `undefined` if there is currently no active
* editor or the active editor widget is neither a text nor a diff editor.
*
* @see `IEditorService.activeEditor`
*/
openEditor(input: IResourceInputType, position?: Position): TPromise<IEditor>;
openEditor(input: IResourceInputType, sideBySide?: boolean): TPromise<IEditor>;
readonly activeTextEditorWidget: ICodeEditor;
/**
* Similar to #openEditor() but allows to open multiple editors for different positions at the same time. If there are
* more than one editor per position, only the first one will be active and the others stacked behind inactive.
* All editors that are currently visible. An editor is visible when it is opened in an
* editor group and active in that group. Multiple editor groups can be opened at the same time.
*/
openEditors(editors: { input: IResourceInputType, position?: Position }[]): TPromise<IEditor[]>;
openEditors(editors: { input: IEditorInput, position?: Position, options?: IEditorOptions | ITextEditorOptions }[]): TPromise<IEditor[]>;
openEditors(editors: { input: IResourceInputType }[], sideBySide?: boolean): TPromise<IEditor[]>;
openEditors(editors: { input: IEditorInput, options?: IEditorOptions | ITextEditorOptions }[], sideBySide?: boolean): TPromise<IEditor[]>;
readonly visibleEditors: ReadonlyArray<IEditorInput>;
/**
* Given a list of editors to replace, will look across all groups where this editor is open (active or hidden)
* and replace it with the new editor and the provied options.
* All editor controls that are currently visible across all editor groups.
*/
replaceEditors(editors: { toReplace: IResourceInputType, replaceWith: IResourceInputType }[], position?: Position): TPromise<IEditor[]>;
replaceEditors(editors: { toReplace: IEditorInput, replaceWith: IEditorInput, options?: IEditorOptions | ITextEditorOptions }[], position?: Position): TPromise<IEditor[]>;
readonly visibleControls: ReadonlyArray<IEditor>;
/**
* Closes the editor at the provided position.
* All text editor widgets that are currently visible across all editor groups. A text editor
* widget is either a text or a diff editor.
*/
closeEditor(position: Position, input: IEditorInput): TPromise<void>;
readonly visibleTextEditorWidgets: ReadonlyArray<ICodeEditor>;
/**
* Closes all editors of the provided groups, or all editors across all groups
* if no position is provided.
* All editors that are opened across all editor groups. This includes active as well as inactive
* editors in each editor group.
*/
closeEditors(positions?: Position[]): TPromise<void>;
readonly editors: ReadonlyArray<IEditorInput>;
/**
* Closes editors of a specific group at the provided position. If the optional editor is provided to exclude, it
* will not be closed. The direction can be used in that case to control if all other editors should get closed,
* or towards a specific direction.
* Open an editor in an editor group.
*
* @param editor the editor to open
* @param options the options to use for the editor
* @param group the target group. If unspecified, the editor will open in the currently
* active group. Use `SIDE_GROUP_TYPE` to open the editor in a new editor group to the side
* of the currently active group.
*/
closeEditors(position: Position, filter?: ICloseEditorsFilter): TPromise<void>;
openEditor(editor: IEditorInput, options?: IEditorOptions | ITextEditorOptions, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): TPromise<IEditor>;
openEditor(editor: IResourceInput | IUntitledResourceInput, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): TPromise<ITextEditor>;
openEditor(editor: IResourceDiffInput, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): TPromise<ITextDiffEditor>;
openEditor(editor: IResourceSideBySideInput, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): TPromise<ITextSideBySideEditor>;
/**
* Closes the provided editors of a specific group at the provided position.
* Open editors in an editor group.
*
* @param editors the editors to open with associated options
* @param group the target group. If unspecified, the editor will open in the currently
* active group. Use `SIDE_GROUP_TYPE` to open the editor in a new editor group to the side
* of the currently active group.
*/
closeEditors(position: Position, editors: IEditorInput[]): TPromise<void>;
openEditors(editors: IEditorInputWithOptions[], group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): TPromise<ReadonlyArray<IEditor>>;
openEditors(editors: IResourceEditor[], group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): TPromise<ReadonlyArray<IEditor>>;
/**
* Closes specific editors across all groups at once.
* Replaces editors in an editor group with the provided replacement.
*
* @param editors the editors to replace
*
* @returns a promise that is resolved when the replaced active
* editor (if any) has finished loading.
*/
closeEditors(editors: { positionOne?: ICloseEditorsFilter, positionTwo?: ICloseEditorsFilter, positionThree?: ICloseEditorsFilter }): TPromise<void>;
replaceEditors(editors: IResourceEditorReplacement[], group: IEditorGroup | GroupIdentifier): TPromise<void>;
replaceEditors(editors: IEditorReplacement[], group: IEditorGroup | GroupIdentifier): TPromise<void>;
/**
* Closes specific editors across all groups at once.
* Find out if the provided editor (or resource of an editor) is opened in any or
* a specific editor group.
*
* Note: An editor can be opened but not actively visible.
*
* @param group optional to specify a group to check for the editor being opened
*/
closeEditors(editors: { positionOne?: IEditorInput[], positionTwo?: IEditorInput[], positionThree?: IEditorInput[] }): TPromise<void>;
isOpen(editor: IEditorInput | IResourceInput | IUntitledResourceInput, group?: IEditorGroup | GroupIdentifier): boolean;
/**
* Allows to resolve an untyped input to a workbench typed instanceof editor input
* Allows to override the opening of editors by installing a handler that will
* be called each time an editor is about to open allowing to override the
* operation to open a different editor.
*/
createInput(input: IResourceInputType): IEditorInput;
}
export interface IEditorPart {
openEditor(input?: IEditorInput, options?: IEditorOptions | ITextEditorOptions, sideBySide?: boolean): TPromise<IEditor>;
openEditor(input?: IEditorInput, options?: IEditorOptions | ITextEditorOptions, position?: Position): TPromise<IEditor>;
openEditors(editors: { input: IEditorInput, position?: Position, options?: IEditorOptions | ITextEditorOptions }[]): TPromise<IEditor[]>;
openEditors(editors: { input: IEditorInput, options?: IEditorOptions | ITextEditorOptions }[], sideBySide?: boolean): TPromise<IEditor[]>;
replaceEditors(editors: { toReplace: IEditorInput, replaceWith: IEditorInput, options?: IEditorOptions | ITextEditorOptions }[], position?: Position): TPromise<IEditor[]>;
closeEditors(positions?: Position[]): TPromise<void>;
closeEditor(position: Position, input: IEditorInput): TPromise<void>;
closeEditors(position: Position, filter?: ICloseEditorsFilter): TPromise<void>;
closeEditors(position: Position, editors: IEditorInput[]): TPromise<void>;
closeEditors(editors: { positionOne?: ICloseEditorsFilter, positionTwo?: ICloseEditorsFilter, positionThree?: ICloseEditorsFilter }): TPromise<void>;
closeEditors(editors: { positionOne?: IEditorInput[], positionTwo?: IEditorInput[], positionThree?: IEditorInput[] }): TPromise<void>;
getActiveEditor(): IEditor;
getVisibleEditors(): IEditor[];
getActiveEditorInput(): IEditorInput;
}
type ICachedEditorInput = ResourceEditorInput | IFileEditorInput | DataUriEditorInput;
export class WorkbenchEditorService implements IWorkbenchEditorService {
public _serviceBrand: any;
private static CACHE: ResourceMap<ICachedEditorInput> = new ResourceMap<ICachedEditorInput>();
private editorPart: IEditorPart | IWorkbenchEditorService;
private fileInputFactory: IFileInputFactory;
constructor(
editorPart: IEditorPart | IWorkbenchEditorService,
@IUntitledEditorService private untitledEditorService: IUntitledEditorService,
@IWorkspaceContextService private workspaceContextService: IWorkspaceContextService,
@IInstantiationService private instantiationService: IInstantiationService,
@IEnvironmentService private environmentService: IEnvironmentService,
@IFileService private fileService: IFileService
) {
this.editorPart = editorPart;
this.fileInputFactory = Registry.as<IEditorInputFactoryRegistry>(EditorExtensions.EditorInputFactories).getFileInputFactory();
}
public getActiveEditor(): IEditor {
return this.editorPart.getActiveEditor();
}
public getActiveEditorInput(): IEditorInput {
return this.editorPart.getActiveEditorInput();
}
public getVisibleEditors(): IEditor[] {
return this.editorPart.getVisibleEditors();
}
public openEditor(input: IEditorInput, options?: IEditorOptions, sideBySide?: boolean): TPromise<IEditor>;
public openEditor(input: IEditorInput, options?: IEditorOptions, position?: Position): TPromise<IEditor>;
public openEditor(input: IResourceInputType, position?: Position): TPromise<IEditor>;
public openEditor(input: IResourceInputType, sideBySide?: boolean): TPromise<IEditor>;
public openEditor(input: any, arg2?: any, arg3?: any): TPromise<IEditor> {
if (!input) {
return TPromise.wrap<IEditor>(null);
}
// Workbench Input Support
if (input instanceof EditorInput) {
return this.doOpenEditor(input, this.toOptions(arg2), arg3);
}
// Support opening foreign resources (such as a http link that points outside of the workbench)
const resourceInput = <IResourceInput>input;
if (resourceInput.resource instanceof URI) {
const schema = resourceInput.resource.scheme;
if (schema === Schemas.http || schema === Schemas.https) {
window.open(resourceInput.resource.toString(true));
return TPromise.wrap<IEditor>(null);
}
}
// Untyped Text Editor Support (required for code that uses this service below workbench level)
const textInput = <IResourceInputType>input;
const typedInput = this.createInput(textInput);
if (typedInput) {
return this.doOpenEditor(typedInput, TextEditorOptions.from(textInput), arg2);
}
return TPromise.wrap<IEditor>(null);
}
private toOptions(options?: IEditorOptions | EditorOptions): EditorOptions {
if (!options || options instanceof EditorOptions) {
return options as EditorOptions;
}
const textOptions: ITextEditorOptions = options;
if (!!textOptions.selection) {
return TextEditorOptions.create(options);
}
return EditorOptions.create(options);
}
/**
* Allow subclasses to implement their own behavior for opening editor (see below).
*/
protected doOpenEditor(input: IEditorInput, options?: EditorOptions, sideBySide?: boolean): TPromise<IEditor>;
protected doOpenEditor(input: IEditorInput, options?: EditorOptions, position?: Position): TPromise<IEditor>;
protected doOpenEditor(input: IEditorInput, options?: EditorOptions, arg3?: any): TPromise<IEditor> {
return this.editorPart.openEditor(input, options, arg3);
}
public openEditors(editors: { input: IResourceInputType, position: Position }[]): TPromise<IEditor[]>;
public openEditors(editors: { input: IEditorInput, position: Position, options?: IEditorOptions }[]): TPromise<IEditor[]>;
public openEditors(editors: { input: IResourceInputType }[], sideBySide?: boolean): TPromise<IEditor[]>;
public openEditors(editors: { input: IEditorInput, options?: IEditorOptions }[], sideBySide?: boolean): TPromise<IEditor[]>;
public openEditors(editors: any[], sideBySide?: boolean): TPromise<IEditor[]> {
const inputs = editors.map(editor => this.createInput(editor.input));
const typedInputs: { input: IEditorInput, position: Position, options?: EditorOptions }[] = inputs.map((input, index) => {
const options = editors[index].input instanceof EditorInput ? this.toOptions(editors[index].options) : TextEditorOptions.from(editors[index].input);
return {
input,
options,
position: editors[index].position
};
});
return this.editorPart.openEditors(typedInputs, sideBySide);
}
public replaceEditors(editors: { toReplace: IResourceInputType, replaceWith: IResourceInputType }[], position?: Position): TPromise<IEditor[]>;
public replaceEditors(editors: { toReplace: IEditorInput, replaceWith: IEditorInput, options?: IEditorOptions }[], position?: Position): TPromise<IEditor[]>;
public replaceEditors(editors: any[], position?: Position): TPromise<IEditor[]> {
const toReplaceInputs = editors.map(editor => this.createInput(editor.toReplace));
const replaceWithInputs = editors.map(editor => this.createInput(editor.replaceWith));
const typedReplacements: { toReplace: IEditorInput, replaceWith: IEditorInput, options?: EditorOptions }[] = editors.map((editor, index) => {
const options = editor.toReplace instanceof EditorInput ? this.toOptions(editor.options) : TextEditorOptions.from(editor.replaceWith);
return {
toReplace: toReplaceInputs[index],
replaceWith: replaceWithInputs[index],
options
};
});
return this.editorPart.replaceEditors(typedReplacements, position);
}
public closeEditor(position: Position, input: IEditorInput): TPromise<void> {
return this.doCloseEditor(position, input);
}
protected doCloseEditor(position: Position, input: IEditorInput): TPromise<void> {
return this.editorPart.closeEditor(position, input);
}
public closeEditors(positions?: Position[]): TPromise<void>;
public closeEditors(position: Position, filter?: ICloseEditorsFilter): TPromise<void>;
public closeEditors(position: Position, editors: IEditorInput[]): TPromise<void>;
public closeEditors(editors: { positionOne?: ICloseEditorsFilter, positionTwo?: ICloseEditorsFilter, positionThree?: ICloseEditorsFilter }): TPromise<void>;
public closeEditors(editors: { positionOne?: IEditorInput[], positionTwo?: IEditorInput[], positionThree?: IEditorInput[] }): TPromise<void>;
public closeEditors(positionsOrEditors: any, filterOrEditors?: any): TPromise<void> {
return this.editorPart.closeEditors(positionsOrEditors, filterOrEditors);
}
public createInput(input: IEditorInput): EditorInput;
public createInput(input: IResourceInputType): EditorInput;
public createInput(input: any): IEditorInput {
// Workbench Input Support
if (input instanceof EditorInput) {
return input;
}
// Side by Side Support
const resourceSideBySideInput = <IResourceSideBySideInput>input;
if (resourceSideBySideInput.masterResource && resourceSideBySideInput.detailResource) {
const masterInput = this.createInput({ resource: resourceSideBySideInput.masterResource });
const detailInput = this.createInput({ resource: resourceSideBySideInput.detailResource });
return new SideBySideEditorInput(resourceSideBySideInput.label || masterInput.getName(), typeof resourceSideBySideInput.description === 'string' ? resourceSideBySideInput.description : masterInput.getDescription(), detailInput, masterInput);
}
// Diff Editor Support
const resourceDiffInput = <IResourceDiffInput>input;
if (resourceDiffInput.leftResource && resourceDiffInput.rightResource) {
const leftInput = this.createInput({ resource: resourceDiffInput.leftResource });
const rightInput = this.createInput({ resource: resourceDiffInput.rightResource });
const label = resourceDiffInput.label || nls.localize('compareLabels', "{0} ↔ {1}", this.toDiffLabel(leftInput, this.workspaceContextService, this.environmentService), this.toDiffLabel(rightInput, this.workspaceContextService, this.environmentService));
return new DiffEditorInput(label, resourceDiffInput.description, leftInput, rightInput);
}
// Untitled file support
const untitledInput = <IUntitledResourceInput>input;
if (!untitledInput.resource || typeof untitledInput.filePath === 'string' || (untitledInput.resource instanceof URI && untitledInput.resource.scheme === Schemas.untitled)) {
// {{SQL CARBON EDIT}}
return this.untitledEditorService.createOrGet(untitledInput.filePath ? URI.file(untitledInput.filePath) : untitledInput.resource, 'sql', untitledInput.contents, untitledInput.encoding);
}
// Resource Editor Support
const resourceInput = <IResourceInput>input;
if (resourceInput.resource instanceof URI) {
let label = resourceInput.label;
if (!label && resourceInput.resource.scheme !== Schemas.data) {
label = basename(resourceInput.resource.fsPath); // derive the label from the path (but not for data URIs)
}
return this.createOrGet(resourceInput.resource, this.instantiationService, label, resourceInput.description, resourceInput.encoding);
}
return null;
}
private createOrGet(resource: URI, instantiationService: IInstantiationService, label: string, description: string, encoding?: string): ICachedEditorInput {
if (WorkbenchEditorService.CACHE.has(resource)) {
const input = WorkbenchEditorService.CACHE.get(resource);
if (input instanceof ResourceEditorInput) {
input.setName(label);
input.setDescription(description);
} else if (!(input instanceof DataUriEditorInput)) {
input.setPreferredEncoding(encoding);
}
return input;
}
let input: ICachedEditorInput;
// File
if (this.fileService.canHandleResource(resource)) {
input = this.fileInputFactory.createFileInput(resource, encoding, instantiationService);
}
// Data URI
else if (resource.scheme === Schemas.data) {
input = instantiationService.createInstance(DataUriEditorInput, label, description, resource);
}
// Resource
else {
input = instantiationService.createInstance(ResourceEditorInput, label, description, resource);
}
WorkbenchEditorService.CACHE.set(resource, input);
once(input.onDispose)(() => {
WorkbenchEditorService.CACHE.delete(resource);
});
return input;
}
private toDiffLabel(input: EditorInput, context: IWorkspaceContextService, environment: IEnvironmentService): string {
const res = input.getResource();
// Do not try to extract any paths from simple untitled editors
if (res.scheme === Schemas.untitled && !this.untitledEditorService.hasAssociatedFilePath(res)) {
return input.getName();
}
// Otherwise: for diff labels prefer to see the path as part of the label
return getPathLabel(res.fsPath, context, environment);
}
}
export interface IEditorOpenHandler {
(input: IEditorInput, options?: EditorOptions, sideBySide?: boolean): TPromise<IEditor>;
(input: IEditorInput, options?: EditorOptions, position?: Position): TPromise<IEditor>;
}
export interface IEditorCloseHandler {
(position: Position, input: IEditorInput): TPromise<void>;
}
/**
* Subclass of workbench editor service that delegates all calls to the provided editor service. Subclasses can choose to override the behavior
* of openEditor() and closeEditor() by providing a handler.
*
* This gives clients a chance to override the behavior of openEditor() and closeEditor().
*/
export class DelegatingWorkbenchEditorService extends WorkbenchEditorService {
private editorOpenHandler: IEditorOpenHandler;
private editorCloseHandler: IEditorCloseHandler;
constructor(
@IUntitledEditorService untitledEditorService: IUntitledEditorService,
@IInstantiationService instantiationService: IInstantiationService,
@IWorkspaceContextService workspaceContextService: IWorkspaceContextService,
@IWorkbenchEditorService editorService: IWorkbenchEditorService,
@IEnvironmentService environmentService: IEnvironmentService,
@IFileService fileService: IFileService
) {
super(
editorService,
untitledEditorService,
workspaceContextService,
instantiationService,
environmentService,
fileService
);
}
public setEditorOpenHandler(handler: IEditorOpenHandler): void {
this.editorOpenHandler = handler;
}
public setEditorCloseHandler(handler: IEditorCloseHandler): void {
this.editorCloseHandler = handler;
}
protected doOpenEditor(input: IEditorInput, options?: EditorOptions, sideBySide?: boolean): TPromise<IEditor>;
protected doOpenEditor(input: IEditorInput, options?: EditorOptions, position?: Position): TPromise<IEditor>;
protected doOpenEditor(input: IEditorInput, options?: EditorOptions, arg3?: any): TPromise<IEditor> {
const handleOpen = this.editorOpenHandler ? this.editorOpenHandler(input, options, arg3) : TPromise.as(void 0);
return handleOpen.then(editor => {
if (editor) {
return TPromise.as<IEditor>(editor);
}
return super.doOpenEditor(input, options, arg3);
});
}
protected doCloseEditor(position: Position, input: IEditorInput): TPromise<void> {
const handleClose = this.editorCloseHandler ? this.editorCloseHandler(position, input) : TPromise.as(void 0);
return handleClose.then(() => {
return super.doCloseEditor(position, input);
});
}
overrideOpenEditor(handler: IOpenEditorOverrideHandler): IDisposable;
/**
* Invoke a function in the context of the services of the active editor.
*/
invokeWithinEditorContext<T>(fn: (accessor: ServicesAccessor) => T): T;
/**
* Converts a lightweight input to a workbench editor input.
*/
createInput(input: IResourceEditor, options?: { forceFileInput: boolean }): IEditorInput;
}

View File

@@ -6,278 +6,8 @@
'use strict';
import * as assert from 'assert';
import { Promise, TPromise } from 'vs/base/common/winjs.base';
import * as paths from 'vs/base/common/paths';
import { Position, IEditor, IEditorInput } 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, TextEditorOptions } from 'vs/workbench/common/editor';
import { FileEditorInput } from 'vs/workbench/parts/files/common/editors/fileEditorInput';
import { workbenchInstantiationService } from 'vs/workbench/test/workbenchTestServices';
import { DelegatingWorkbenchEditorService, WorkbenchEditorService, IEditorPart } from 'vs/workbench/services/editor/common/editorService';
import { UntitledEditorInput } from 'vs/workbench/common/editor/untitledEditorInput';
import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput';
import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService';
import { ICloseEditorsFilter } from 'vs/workbench/browser/parts/editor/editorPart';
import { snapshotToString } from 'vs/platform/files/common/files';
let activeEditor: BaseEditor = {
getSelection: function () {
return 'test.selection';
}
} as any;
let openedEditorInput: EditorInput;
let openedEditorOptions: EditorOptions;
function toResource(path: string) {
return URI.from({ scheme: 'custom', path });
}
function toFileResource(self: any, path: string) {
return URI.file(paths.join('C:\\', Buffer.from(self.test.fullTitle()).toString('base64'), path));
}
class TestEditorPart implements IEditorPart {
private activeInput: EditorInput;
public getId(): string {
return null;
}
public openEditors(args: any[]): Promise {
return TPromise.as([]);
}
public replaceEditors(editors: { toReplace: EditorInput, replaceWith: EditorInput, options?: any }[]): TPromise<BaseEditor[]> {
return TPromise.as([]);
}
public closeEditors(positions?: Position[]): TPromise<void>;
public closeEditors(position: Position, filter?: ICloseEditorsFilter): TPromise<void>;
public closeEditors(position: Position, editors?: EditorInput[]): TPromise<void>;
public closeEditors(editors: { positionOne?: ICloseEditorsFilter, positionTwo?: ICloseEditorsFilter, positionThree?: ICloseEditorsFilter }): TPromise<void>;
public closeEditors(editors: { positionOne?: EditorInput[], positionTwo?: EditorInput[], positionThree?: EditorInput[] }): TPromise<void>;
public closeEditors(positionOrEditors: any, filterOrEditors?: any): TPromise<void> {
return TPromise.as(null);
}
public closeEditor(position: Position, input: EditorInput): TPromise<void> {
return TPromise.as(null);
}
public openEditor(input?: EditorInput, options?: EditorOptions, sideBySide?: boolean): TPromise<BaseEditor>;
public openEditor(input?: EditorInput, options?: EditorOptions, position?: Position): TPromise<BaseEditor>;
public openEditor(input?: EditorInput, options?: EditorOptions, arg?: any): TPromise<BaseEditor> {
openedEditorInput = input;
openedEditorOptions = options;
return TPromise.as(activeEditor);
}
public getActiveEditor(): BaseEditor {
return activeEditor;
}
public setActiveEditorInput(input: EditorInput) {
this.activeInput = input;
}
public getActiveEditorInput(): EditorInput {
return this.activeInput;
}
public getVisibleEditors(): IEditor[] {
return [activeEditor];
}
}
suite('WorkbenchEditorService', () => {
suite('Editor service', () => {
test('basics', function () {
let instantiationService = workbenchInstantiationService();
let activeInput: EditorInput = instantiationService.createInstance(FileEditorInput, toFileResource(this, '/something.js'), void 0);
let testEditorPart = new TestEditorPart();
testEditorPart.setActiveEditorInput(activeInput);
let service: WorkbenchEditorService = <any>instantiationService.createInstance(<any>WorkbenchEditorService, testEditorPart);
assert.strictEqual(service.getActiveEditor(), activeEditor);
assert.strictEqual(service.getActiveEditorInput(), activeInput);
// Open EditorInput
service.openEditor(activeInput, null).then((editor) => {
assert.strictEqual(openedEditorInput, activeInput);
assert.strictEqual(openedEditorOptions, null);
assert.strictEqual(editor, activeEditor);
assert.strictEqual(service.getVisibleEditors().length, 1);
assert(service.getVisibleEditors()[0] === editor);
});
service.openEditor(activeInput, null, Position.ONE).then((editor) => {
assert.strictEqual(openedEditorInput, activeInput);
assert.strictEqual(openedEditorOptions, null);
assert.strictEqual(editor, activeEditor);
assert.strictEqual(service.getVisibleEditors().length, 1);
assert(service.getVisibleEditors()[0] === editor);
});
// Open Untyped Input (file)
service.openEditor({ resource: toFileResource(this, '/index.html'), options: { selection: { startLineNumber: 1, startColumn: 1 } } }).then((editor) => {
assert.strictEqual(editor, activeEditor);
assert(openedEditorInput instanceof FileEditorInput);
let contentInput = <FileEditorInput>openedEditorInput;
assert.strictEqual(contentInput.getResource().fsPath, toFileResource(this, '/index.html').fsPath);
assert(openedEditorOptions instanceof TextEditorOptions);
let textEditorOptions = <TextEditorOptions>openedEditorOptions;
assert(textEditorOptions.hasOptionsDefined());
});
// Open Untyped Input (file, encoding)
service.openEditor({ resource: toFileResource(this, '/index.html'), encoding: 'utf16le', options: { selection: { startLineNumber: 1, startColumn: 1 } } }).then((editor) => {
assert.strictEqual(editor, activeEditor);
assert(openedEditorInput instanceof FileEditorInput);
let contentInput = <FileEditorInput>openedEditorInput;
assert.equal(contentInput.getPreferredEncoding(), 'utf16le');
});
// Open Untyped Input (untitled)
service.openEditor({ options: { selection: { startLineNumber: 1, startColumn: 1 } } }).then((editor) => {
assert.strictEqual(editor, activeEditor);
assert(openedEditorInput instanceof UntitledEditorInput);
assert(openedEditorOptions instanceof TextEditorOptions);
let textEditorOptions = <TextEditorOptions>openedEditorOptions;
assert(textEditorOptions.hasOptionsDefined());
});
// Open Untyped Input (untitled with contents)
service.openEditor({ contents: 'Hello Untitled', options: { selection: { startLineNumber: 1, startColumn: 1 } } }).then((editor) => {
assert.strictEqual(editor, activeEditor);
assert(openedEditorInput instanceof UntitledEditorInput);
const untitledInput = openedEditorInput as UntitledEditorInput;
untitledInput.resolve().then(model => {
assert.equal(snapshotToString(model.createSnapshot()), 'Hello Untitled');
});
});
// Open Untyped Input (untitled with file path)
service.openEditor({ filePath: '/some/path.txt', options: { selection: { startLineNumber: 1, startColumn: 1 } } }).then((editor) => {
assert.strictEqual(editor, activeEditor);
assert(openedEditorInput instanceof UntitledEditorInput);
const untitledInput = openedEditorInput as UntitledEditorInput;
assert.ok(untitledInput.hasAssociatedFilePath);
});
});
test('caching', function () {
let instantiationService = workbenchInstantiationService();
let activeInput: EditorInput = instantiationService.createInstance(FileEditorInput, toFileResource(this, '/something.js'), void 0);
let testEditorPart = new TestEditorPart();
testEditorPart.setActiveEditorInput(activeInput);
let service: WorkbenchEditorService = <any>instantiationService.createInstance(<any>WorkbenchEditorService, testEditorPart);
// Cached Input (Files)
const fileResource1 = toFileResource(this, '/foo/bar/cache1.js');
const fileInput1 = service.createInput({ resource: fileResource1 });
assert.ok(fileInput1);
const fileResource2 = toFileResource(this, '/foo/bar/cache2.js');
const fileInput2 = service.createInput({ resource: fileResource2 });
assert.ok(fileInput2);
assert.notEqual(fileInput1, fileInput2);
const fileInput1Again = service.createInput({ resource: fileResource1 });
assert.equal(fileInput1Again, fileInput1);
fileInput1Again.dispose();
assert.ok(fileInput1.isDisposed());
const fileInput1AgainAndAgain = service.createInput({ resource: fileResource1 });
assert.notEqual(fileInput1AgainAndAgain, fileInput1);
assert.ok(!fileInput1AgainAndAgain.isDisposed());
// Cached Input (Resource)
const resource1 = toResource.call(this, '/foo/bar/cache1.js');
const input1 = service.createInput({ resource: resource1 });
assert.ok(input1);
const resource2 = toResource.call(this, '/foo/bar/cache2.js');
const input2 = service.createInput({ resource: resource2 });
assert.ok(input2);
assert.notEqual(input1, input2);
const input1Again = service.createInput({ resource: resource1 });
assert.equal(input1Again, input1);
input1Again.dispose();
assert.ok(input1.isDisposed());
const input1AgainAndAgain = service.createInput({ resource: resource1 });
assert.notEqual(input1AgainAndAgain, input1);
assert.ok(!input1AgainAndAgain.isDisposed());
});
test('delegate', function (done) {
let instantiationService = workbenchInstantiationService();
let activeInput: EditorInput = instantiationService.createInstance(FileEditorInput, toFileResource(this, '/something.js'), void 0);
let testEditorPart = new TestEditorPart();
testEditorPart.setActiveEditorInput(activeInput);
instantiationService.createInstance(<any>WorkbenchEditorService, testEditorPart);
class MyEditor extends BaseEditor {
constructor(id: string) {
super(id, null, new TestThemeService());
}
getId(): string {
return 'myEditor';
}
public layout(): void {
}
public createEditor(): any {
}
}
let ed = instantiationService.createInstance(MyEditor, 'my.editor');
let inp = instantiationService.createInstance(ResourceEditorInput, 'name', 'description', URI.parse('my://resource'));
let delegate = instantiationService.createInstance(DelegatingWorkbenchEditorService);
delegate.setEditorOpenHandler((input: IEditorInput, options?: EditorOptions) => {
assert.strictEqual(input, inp);
return TPromise.as(ed);
});
delegate.setEditorCloseHandler((position, input) => {
assert.strictEqual(input, inp);
done();
return TPromise.as(void 0);
});
delegate.openEditor(inp);
delegate.closeEditor(0, inp);
});
});

View File

@@ -9,6 +9,7 @@ import { TPromise } from 'vs/base/common/winjs.base';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IExtensionPoint } from 'vs/workbench/services/extensions/common/extensionsRegistry';
import { Event } from 'vs/base/common/event';
import URI from 'vs/base/common/uri';
export interface IExtensionDescription {
readonly id: string;
@@ -18,7 +19,8 @@ export interface IExtensionDescription {
readonly version: string;
readonly publisher: string;
readonly isBuiltin: boolean;
readonly extensionFolderPath: string;
readonly isUnderDevelopment: boolean;
readonly extensionLocation: URI;
readonly extensionDependencies?: string[];
readonly activationEvents?: string[];
readonly engines: {
@@ -40,7 +42,6 @@ export const IExtensionService = createDecorator<IExtensionService>('extensionSe
export interface IMessage {
type: Severity;
message: string;
source: string;
extensionId: string;
extensionPointId: string;
}

View File

@@ -36,7 +36,6 @@ export class ExtensionMessageCollector {
this._messageHandler({
type: type,
message: message,
source: this._extension.extensionFolderPath,
extensionId: this._extension.id,
extensionPointId: this._extensionPointId
});
@@ -221,6 +220,16 @@ const schema: IJSONSchema = {
description: nls.localize('vscode.extension.activationEvents.workspaceContains', 'An activation event emitted whenever a folder is opened that contains at least a file matching the specified glob pattern.'),
body: 'workspaceContains:${4:filePattern}'
},
{
label: 'onFileSystem',
description: nls.localize('vscode.extension.activationEvents.onFileSystem', 'An activation event emitted whenever a file or folder is accessed with the given scheme.'),
body: 'onFileSystem:${1:scheme}'
},
{
label: 'onSearch',
description: nls.localize('vscode.extension.activationEvents.onSearch', 'An activation event emitted whenever a search is started in the folder with the given scheme.'),
body: 'onSearch:${7:scheme}'
},
{
label: 'onView',
body: 'onView:${5:viewId}',
@@ -289,6 +298,15 @@ const schema: IJSONSchema = {
pattern: EXTENSION_IDENTIFIER_PATTERN
}
},
extensionPack: {
description: nls.localize('vscode.extension.contributes.extensionPack', "A set of extensions that can be installed together. The identifier of an extension is always ${publisher}.${name}. For example: vscode.csharp."),
type: 'array',
uniqueItems: true,
items: {
type: 'string',
pattern: EXTENSION_IDENTIFIER_PATTERN
}
},
scripts: {
type: 'object',
properties: {

View File

@@ -25,13 +25,13 @@ import { generateRandomPipeName, Protocol } from 'vs/base/parts/ipc/node/ipc.net
import { createServer, Server, Socket } from 'net';
import { Event, Emitter, debounceEvent, mapEvent, anyEvent, fromNodeEventEmitter } from 'vs/base/common/event';
import { IInitData, IWorkspaceData, IConfigurationInitData } from 'vs/workbench/api/node/extHost.protocol';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { IExtensionDescription } from 'vs/workbench/services/extensions/common/extensions';
import { IWorkspaceConfigurationService } from 'vs/workbench/services/configuration/common/configuration';
import { ICrashReporterService } from 'vs/workbench/services/crashReporter/electron-browser/crashReporterService';
import { IBroadcastService, IBroadcast } from 'vs/platform/broadcast/electron-browser/broadcastService';
import { isEqual } from 'vs/base/common/paths';
import { EXTENSION_CLOSE_EXTHOST_BROADCAST_CHANNEL, EXTENSION_RELOAD_BROADCAST_CHANNEL, EXTENSION_ATTACH_BROADCAST_CHANNEL, EXTENSION_LOG_BROADCAST_CHANNEL, EXTENSION_TERMINATE_BROADCAST_CHANNEL } from 'vs/platform/extensions/common/extensionHost';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle';
import { IRemoteConsoleLog, log, parse } from 'vs/base/node/console';
import { getScopes } from 'vs/platform/configuration/common/configurationRegistry';
import { ILogService } from 'vs/platform/log/common/log';
@@ -61,7 +61,7 @@ export class ExtensionHostProcessWorker {
private _messageProtocol: TPromise<IMessagePassingProtocol>;
constructor(
/* intentionally not injected */private readonly _extensionService: IExtensionService,
private readonly _extensions: TPromise<IExtensionDescription[]>,
@IWorkspaceContextService private readonly _contextService: IWorkspaceContextService,
@INotificationService private readonly _notificationService: INotificationService,
@IWindowsService private readonly _windowsService: IWindowsService,
@@ -96,11 +96,9 @@ export class ExtensionHostProcessWorker {
const globalExitListener = () => this.terminate();
process.once('exit', globalExitListener);
this._toDispose.push({
dispose: () => {
process.removeListener('exit', globalExitListener);
}
});
this._toDispose.push(toDisposable(() => {
process.removeListener('exit', globalExitListener);
}));
}
public dispose(): void {
@@ -143,7 +141,8 @@ export class ExtensionHostProcessWorker {
VERBOSE_LOGGING: true,
VSCODE_IPC_HOOK_EXTHOST: pipeName,
VSCODE_HANDLES_UNCAUGHT_ERRORS: true,
VSCODE_LOG_STACK: !this._isExtensionDevTestFromCli && (this._isExtensionDevHost || !this._environmentService.isBuilt || product.quality !== 'stable' || this._environmentService.verbose)
VSCODE_LOG_STACK: !this._isExtensionDevTestFromCli && (this._isExtensionDevHost || !this._environmentService.isBuilt || product.quality !== 'stable' || this._environmentService.verbose),
VSCODE_LOG_LEVEL: this._environmentService.verbose ? 'trace' : this._environmentService.log
}),
// We only detach the extension host on windows. Linux and Mac orphan by default
// and detach under Linux and Mac create another process group.
@@ -193,13 +192,13 @@ export class ExtensionHostProcessWorker {
}, 100);
// Print out extension host output
onDebouncedOutput(data => {
const inspectorUrlIndex = !this._environmentService.isBuilt && data.data && data.data.indexOf('chrome-devtools://');
if (inspectorUrlIndex >= 0) {
console.log(`%c[Extension Host] %cdebugger inspector at ${data.data.substr(inspectorUrlIndex)}`, 'color: blue', 'color: black');
onDebouncedOutput(output => {
const inspectorUrlMatch = !this._environmentService.isBuilt && output.data && output.data.match(/ws:\/\/([^\s]+)/);
if (inspectorUrlMatch) {
console.log(`%c[Extension Host] %cdebugger inspector at chrome-devtools://devtools/bundled/inspector.html?experiments=true&v8only=true&ws=${inspectorUrlMatch[1]}`, 'color: blue', 'color: black');
} else {
console.group('Extension Host');
console.log(data.data, ...data.format);
console.log(output.data, ...output.format);
console.groupEnd();
}
});
@@ -361,7 +360,7 @@ export class ExtensionHostProcessWorker {
}
private _createExtHostInitData(): TPromise<IInitData> {
return TPromise.join<any>([this._telemetryService.getTelemetryInfo(), this._extensionService.getExtensions()]).then(([telemetryInfo, extensionDescriptions]) => {
return TPromise.join([this._telemetryService.getTelemetryInfo(), this._extensions]).then(([telemetryInfo, extensionDescriptions]) => {
const configurationData: IConfigurationInitData = { ...this._configurationService.getConfigurationData(), configurationScopes: {} };
const r: IInitData = {
parentPid: process.pid,
@@ -369,7 +368,6 @@ export class ExtensionHostProcessWorker {
isExtensionDevelopmentDebug: this._isExtensionDevDebug,
appRoot: this._environmentService.appRoot,
appSettingsHome: this._environmentService.appSettingsHome,
disableExtensions: this._environmentService.disableExtensions,
extensionDevelopmentPath: this._environmentService.extensionDevelopmentPath,
extensionTestsPath: this._environmentService.extensionTestsPath
},

View File

@@ -35,7 +35,7 @@ export class ExtensionHostProfiler {
private distill(profile: Profile, extensions: IExtensionDescription[]): IExtensionHostProfile {
let searchTree = TernarySearchTree.forPaths<IExtensionDescription>();
for (let extension of extensions) {
searchTree.set(realpathSync(extension.extensionFolderPath), extension);
searchTree.set(realpathSync(extension.extensionLocation.fsPath), extension);
}
let nodes = profile.nodes;
@@ -82,7 +82,7 @@ export class ExtensionHostProfiler {
let distilledIds: ProfileSegmentId[] = [];
let currSegmentTime = 0;
let currSegmentId = void 0;
let currSegmentId: string = void 0;
for (let i = 0; i < samples.length; i++) {
let id = samples[i];
let segmentId = idsToSegmentId.get(id);

View File

@@ -20,7 +20,7 @@ import { USER_MANIFEST_CACHE_FILE, BUILTIN_MANIFEST_CACHE_FILE, MANIFEST_CACHE_F
import { IExtensionEnablementService, IExtensionIdentifier, EnablementState, IExtensionManagementService, LocalExtensionType } from 'vs/platform/extensionManagement/common/extensionManagement';
import { areSameExtensions, BetterMergeId, BetterMergeDisabledNowKey, getGalleryExtensionIdFromLocal } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { ExtensionsRegistry, ExtensionPoint, IExtensionPointUser, ExtensionMessageCollector, IExtensionPoint } from 'vs/workbench/services/extensions/common/extensionsRegistry';
import { ExtensionScanner, ILog, ExtensionScannerInput, IExtensionResolver, IExtensionReference, Translations } from 'vs/workbench/services/extensions/node/extensionPoints';
import { ExtensionScanner, ILog, ExtensionScannerInput, IExtensionResolver, IExtensionReference, Translations, IRelaxedExtensionDescription } from 'vs/workbench/services/extensions/node/extensionPoints';
import { ProxyIdentifier } from 'vs/workbench/services/extensions/node/proxyIdentifier';
import { ExtHostContext, ExtHostExtensionServiceShape, IExtHostContext, MainContext } from 'vs/workbench/api/node/extHost.protocol';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
@@ -32,7 +32,7 @@ import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc';
import { ExtHostCustomersRegistry } from 'vs/workbench/api/electron-browser/extHostCustomers';
import { IWindowService } from 'vs/platform/windows/common/windows';
import { IDisposable, Disposable } from 'vs/base/common/lifecycle';
import { mark, time } from 'vs/base/common/performance';
import { mark } from 'vs/base/common/performance';
import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
import { Barrier } from 'vs/base/common/async';
import { Event, Emitter } from 'vs/base/common/event';
@@ -42,6 +42,7 @@ import * as strings from 'vs/base/common/strings';
import { RPCProtocol } from 'vs/workbench/services/extensions/node/rpcProtocol';
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
import { isFalsyOrEmpty } from 'vs/base/common/arrays';
import { Schemas } from 'vs/base/common/network';
let _SystemExtensionsRoot: string = null;
function getSystemExtensionsRoot(): string {
@@ -97,11 +98,7 @@ class ExtraBuiltInExtensionResolver implements IExtensionResolver {
// Enable to see detailed message communication between window and extension host
const logExtensionHostCommunication = false;
function messageWithSource(msg: IMessage): string {
return messageWithSource2(msg.source, msg.message);
}
function messageWithSource2(source: string, message: string): string {
function messageWithSource(source: string, message: string): string {
if (source) {
return `[${source}]: ${message}`;
}
@@ -119,15 +116,13 @@ export class ExtensionHostProcessManager extends Disposable {
* A map of already activated events to speed things up if the same activation event is triggered multiple times.
*/
private readonly _extensionHostProcessFinishedActivateEvents: { [activationEvent: string]: boolean; };
private readonly _extensionHostProcessActivationTimes: { [id: string]: ActivationTimes; };
private readonly _extensionHostExtensionRuntimeErrors: { [id: string]: Error[]; };
private _extensionHostProcessRPCProtocol: RPCProtocol;
private readonly _extensionHostProcessCustomers: IDisposable[];
private readonly _extensionHostProcessWorker: ExtensionHostProcessWorker;
/**
* winjs believes a proxy is a promise because it has a `then` method, so wrap the result in an object.
*/
private readonly _extensionHostProcessProxy: TPromise<{ value: ExtHostExtensionServiceShape; }>;
private _extensionHostProcessProxy: TPromise<{ value: ExtHostExtensionServiceShape; }>;
constructor(
extensionHostProcessWorker: ExtensionHostProcessWorker,
@@ -137,11 +132,8 @@ export class ExtensionHostProcessManager extends Disposable {
) {
super();
this._extensionHostProcessFinishedActivateEvents = Object.create(null);
this._extensionHostProcessActivationTimes = Object.create(null);
this._extensionHostExtensionRuntimeErrors = Object.create(null);
this._extensionHostProcessRPCProtocol = null;
this._extensionHostProcessCustomers = [];
this._extensionHostProcessProxy = null;
this._extensionHostProcessWorker = extensionHostProcessWorker;
this.onDidCrash = this._extensionHostProcessWorker.onCrashed;
@@ -175,22 +167,11 @@ export class ExtensionHostProcessManager extends Disposable {
errors.onUnexpectedError(err);
}
}
this._extensionHostProcessProxy = null;
super.dispose();
}
public getActivatedExtensionIds(): string[] {
return Object.keys(this._extensionHostProcessActivationTimes);
}
public getActivationTimes(): { [id: string]: ActivationTimes; } {
return this._extensionHostProcessActivationTimes;
}
public getRuntimeErrors(): { [id: string]: Error[]; } {
return this._extensionHostExtensionRuntimeErrors;
}
public canProfileExtensionHost(): boolean {
return this._extensionHostProcessWorker && Boolean(this._extensionHostProcessWorker.getInspectPort());
}
@@ -202,7 +183,11 @@ export class ExtensionHostProcessManager extends Disposable {
}
this._extensionHostProcessRPCProtocol = new RPCProtocol(protocol);
const extHostContext: IExtHostContext = this._extensionHostProcessRPCProtocol;
const extHostContext: IExtHostContext = {
getProxy: <T>(identifier: ProxyIdentifier<T>): T => this._extensionHostProcessRPCProtocol.getProxy(identifier),
set: <T, R extends T>(identifier: ProxyIdentifier<T>, instance: R): R => this._extensionHostProcessRPCProtocol.set(identifier, instance),
assertRegistered: (identifiers: ProxyIdentifier<any>[]): void => this._extensionHostProcessRPCProtocol.assertRegistered(identifiers),
};
// Named customers
const namedCustomers = ExtHostCustomersRegistry.getNamedCustomers();
@@ -222,7 +207,7 @@ export class ExtensionHostProcessManager extends Disposable {
}
// Check that no named customers are missing
const expected: ProxyIdentifier<any>[] = Object.keys(MainContext).map((key) => MainContext[key]);
const expected: ProxyIdentifier<any>[] = Object.keys(MainContext).map((key) => (<any>MainContext)[key]);
this._extensionHostProcessRPCProtocol.assertRegistered(expected);
return this._extensionHostProcessRPCProtocol.getProxy(ExtHostContext.ExtHostExtensionService);
@@ -233,6 +218,11 @@ export class ExtensionHostProcessManager extends Disposable {
return NO_OP_VOID_PROMISE;
}
return this._extensionHostProcessProxy.then((proxy) => {
if (!proxy) {
// this case is already covered above and logged.
// i.e. the extension host could not be started
return NO_OP_VOID_PROMISE;
}
return proxy.value.$activateByEvent(activationEvent);
}).then(() => {
this._extensionHostProcessFinishedActivateEvents[activationEvent] = true;
@@ -248,20 +238,10 @@ export class ExtensionHostProcessManager extends Disposable {
}
throw new Error('Extension host not running or no inspect port available');
}
public onExtensionActivated(extensionId: string, startup: boolean, codeLoadingTime: number, activateCallTime: number, activateResolvedTime: number, activationEvent: string): void {
this._extensionHostProcessActivationTimes[extensionId] = new ActivationTimes(startup, codeLoadingTime, activateCallTime, activateResolvedTime, activationEvent);
}
public onExtensionRuntimeError(extensionId: string, err: Error): void {
if (!this._extensionHostExtensionRuntimeErrors[extensionId]) {
this._extensionHostExtensionRuntimeErrors[extensionId] = [];
}
this._extensionHostExtensionRuntimeErrors[extensionId].push(err);
}
}
export class ExtensionService extends Disposable implements IExtensionService {
public _serviceBrand: any;
private readonly _onDidRegisterExtensions: Emitter<void>;
@@ -276,7 +256,9 @@ export class ExtensionService extends Disposable implements IExtensionService {
public readonly onDidChangeExtensionsStatus: Event<string[]> = this._onDidChangeExtensionsStatus.event;
// --- Members used per extension host process
private _extensionHostProcessManager: ExtensionHostProcessManager;
private _extensionHostProcessManagers: ExtensionHostProcessManager[];
private _extensionHostProcessActivationTimes: { [id: string]: ActivationTimes; };
private _extensionHostExtensionRuntimeErrors: { [id: string]: Error[]; };
constructor(
@IInstantiationService private readonly _instantiationService: IInstantiationService,
@@ -298,11 +280,19 @@ export class ExtensionService extends Disposable implements IExtensionService {
this._onDidRegisterExtensions = new Emitter<void>();
this._extensionHostProcessManager = null;
this._extensionHostProcessManagers = [];
this._extensionHostProcessActivationTimes = Object.create(null);
this._extensionHostExtensionRuntimeErrors = Object.create(null);
this.startDelayed(lifecycleService);
if (this._environmentService.disableExtensions) {
this._notificationService.info(nls.localize('extensionsDisabled', "All extensions are disabled."));
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."), [{
label: nls.localize('Reload', "Reload"),
run: () => {
this._windowService.reloadWindow();
}
}]);
}
}
@@ -357,13 +347,14 @@ export class ExtensionService extends Disposable implements IExtensionService {
}
private _stopExtensionHostProcess(): void {
let previouslyActivatedExtensionIds: string[] = [];
const previouslyActivatedExtensionIds = Object.keys(this._extensionHostProcessActivationTimes);
if (this._extensionHostProcessManager) {
previouslyActivatedExtensionIds = this._extensionHostProcessManager.getActivatedExtensionIds();
this._extensionHostProcessManager.dispose();
this._extensionHostProcessManager = null;
for (let i = 0; i < this._extensionHostProcessManagers.length; i++) {
this._extensionHostProcessManagers[i].dispose();
}
this._extensionHostProcessManagers = [];
this._extensionHostProcessActivationTimes = Object.create(null);
this._extensionHostExtensionRuntimeErrors = Object.create(null);
if (previouslyActivatedExtensionIds.length > 0) {
this._onDidChangeExtensionsStatus.fire(previouslyActivatedExtensionIds);
@@ -373,9 +364,10 @@ export class ExtensionService extends Disposable implements IExtensionService {
private _startExtensionHostProcess(initialActivationEvents: string[]): void {
this._stopExtensionHostProcess();
const extHostProcessWorker = this._instantiationService.createInstance(ExtensionHostProcessWorker, this);
this._extensionHostProcessManager = this._instantiationService.createInstance(ExtensionHostProcessManager, extHostProcessWorker, initialActivationEvents);
this._extensionHostProcessManager.onDidCrash(([code, signal]) => this._onExtensionHostCrashed(code, signal));
const extHostProcessWorker = this._instantiationService.createInstance(ExtensionHostProcessWorker, this.getExtensions());
const extHostProcessManager = this._instantiationService.createInstance(ExtensionHostProcessManager, extHostProcessWorker, initialActivationEvents);
extHostProcessManager.onDidCrash(([code, signal]) => this._onExtensionHostCrashed(code, signal));
this._extensionHostProcessManagers.push(extHostProcessManager);
}
private _onExtensionHostCrashed(code: number, signal: string): void {
@@ -425,10 +417,9 @@ export class ExtensionService extends Disposable implements IExtensionService {
}
private _activateByEvent(activationEvent: string): TPromise<void> {
if (this._extensionHostProcessManager) {
return this._extensionHostProcessManager.activateByEvent(activationEvent);
}
return NO_OP_VOID_PROMISE;
return TPromise.join(
this._extensionHostProcessManagers.map(extHostManager => extHostManager.activateByEvent(activationEvent))
).then(() => { });
}
public whenInstalledExtensionsRegistered(): TPromise<boolean> {
@@ -459,9 +450,6 @@ export class ExtensionService extends Disposable implements IExtensionService {
}
public getExtensionsStatus(): { [id: string]: IExtensionsStatus; } {
const activationTimes = this._extensionHostProcessManager ? this._extensionHostProcessManager.getActivationTimes() : {};
const runtimeErrors = this._extensionHostProcessManager ? this._extensionHostProcessManager.getRuntimeErrors() : {};
let result: { [id: string]: IExtensionsStatus; } = Object.create(null);
if (this._registry) {
const extensions = this._registry.getAllExtensionDescriptions();
@@ -470,8 +458,8 @@ export class ExtensionService extends Disposable implements IExtensionService {
const id = extension.id;
result[id] = {
messages: this._extensionsMessages[id],
activationTimes: activationTimes[id],
runtimeErrors: runtimeErrors[id],
activationTimes: this._extensionHostProcessActivationTimes[id],
runtimeErrors: this._extensionHostExtensionRuntimeErrors[id],
};
}
}
@@ -479,15 +467,21 @@ export class ExtensionService extends Disposable implements IExtensionService {
}
public canProfileExtensionHost(): boolean {
if (this._extensionHostProcessManager) {
return this._extensionHostProcessManager.canProfileExtensionHost();
for (let i = 0, len = this._extensionHostProcessManagers.length; i < len; i++) {
const extHostProcessManager = this._extensionHostProcessManagers[i];
if (extHostProcessManager.canProfileExtensionHost()) {
return true;
}
}
return false;
}
public startExtensionHostProfile(): TPromise<ProfileSession> {
if (this._extensionHostProcessManager) {
return this._extensionHostProcessManager.startExtensionHostProfile();
for (let i = 0, len = this._extensionHostProcessManagers.length; i < len; i++) {
const extHostProcessManager = this._extensionHostProcessManagers[i];
if (extHostProcessManager.canProfileExtensionHost()) {
return extHostProcessManager.startExtensionHostProfile();
}
}
throw new Error('Extension host not running or no inspect port available');
}
@@ -498,9 +492,10 @@ export class ExtensionService extends Disposable implements IExtensionService {
private _scanAndHandleExtensions(): void {
this._getRuntimeExtensions()
.then(runtimeExtensons => {
this._registry = new ExtensionDescriptionRegistry(runtimeExtensons);
this._scanExtensions()
.then(allExtensions => this._getRuntimeExtensions(allExtensions))
.then(allExtensions => {
this._registry = new ExtensionDescriptionRegistry(allExtensions);
let availableExtensions = this._registry.getAllExtensionDescriptions();
let extensionPoints = ExtensionsRegistry.getExtensionPoints();
@@ -508,12 +503,7 @@ export class ExtensionService extends Disposable implements IExtensionService {
let messageHandler = (msg: IMessage) => this._handleExtensionPointMessage(msg);
for (let i = 0, len = extensionPoints.length; i < len; i++) {
const clock = time(`handleExtensionPoint:${extensionPoints[i].name}`);
try {
ExtensionService._handleExtensionPoint(extensionPoints[i], availableExtensions, messageHandler);
} finally {
clock.stop();
}
ExtensionService._handleExtensionPoint(extensionPoints[i], availableExtensions, messageHandler);
}
mark('extensionHostReady');
@@ -523,114 +513,129 @@ export class ExtensionService extends Disposable implements IExtensionService {
});
}
private _getRuntimeExtensions(): TPromise<IExtensionDescription[]> {
private _scanExtensions(): TPromise<IExtensionDescription[]> {
const log = new Logger((severity, source, message) => {
this._logOrShowMessage(severity, this._isDev ? messageWithSource2(source, message) : message);
this._logOrShowMessage(severity, this._isDev ? messageWithSource(source, message) : message);
});
return ExtensionService._scanInstalledExtensions(this._windowService, this._notificationService, this._environmentService, log)
return ExtensionService._scanInstalledExtensions(this._windowService, this._notificationService, this._environmentService, this._extensionEnablementService, log)
.then(({ system, user, development }) => {
return this._extensionEnablementService.getDisabledExtensions()
.then(disabledExtensions => {
let result: { [extensionId: string]: IExtensionDescription; } = {};
let extensionsToDisable: IExtensionIdentifier[] = [];
let userMigratedSystemExtensions: IExtensionIdentifier[] = [{ id: BetterMergeId }];
system.forEach((systemExtension) => {
if (disabledExtensions.every(disabled => !areSameExtensions(disabled, systemExtension))) {
result[systemExtension.id] = systemExtension;
}
});
user.forEach((userExtension) => {
if (result.hasOwnProperty(userExtension.id)) {
log.warn(userExtension.extensionFolderPath, nls.localize('overwritingExtension', "Overwriting extension {0} with {1}.", result[userExtension.id].extensionFolderPath, userExtension.extensionFolderPath));
}
if (disabledExtensions.every(disabled => !areSameExtensions(disabled, userExtension))) {
// Check if the extension is changed to system extension
let userMigratedSystemExtension = userMigratedSystemExtensions.filter(userMigratedSystemExtension => areSameExtensions(userMigratedSystemExtension, { id: userExtension.id }))[0];
if (userMigratedSystemExtension) {
extensionsToDisable.push(userMigratedSystemExtension);
} else {
result[userExtension.id] = userExtension;
}
}
});
development.forEach(developedExtension => {
log.info('', nls.localize('extensionUnderDevelopment', "Loading development extension at {0}", developedExtension.extensionFolderPath));
if (result.hasOwnProperty(developedExtension.id)) {
log.warn(developedExtension.extensionFolderPath, nls.localize('overwritingExtension', "Overwriting extension {0} with {1}.", result[developedExtension.id].extensionFolderPath, developedExtension.extensionFolderPath));
}
// Do not disable extensions under development
result[developedExtension.id] = developedExtension;
});
const runtimeExtensions = Object.keys(result).map(name => result[name]);
this._telemetryService.publicLog('extensionsScanned', {
totalCount: runtimeExtensions.length,
disabledCount: disabledExtensions.length
});
if (extensionsToDisable.length) {
return this.extensionManagementService.getInstalled(LocalExtensionType.User)
.then(installed => {
const toDisable = installed.filter(i => extensionsToDisable.some(e => areSameExtensions({ id: getGalleryExtensionIdFromLocal(i) }, e)));
return TPromise.join(toDisable.map(e => this._extensionEnablementService.setEnablement(e, EnablementState.Disabled)));
})
.then(() => {
this._storageService.store(BetterMergeDisabledNowKey, true);
return runtimeExtensions;
});
} else {
return runtimeExtensions;
}
});
}).then(extensions => this._updateEnableProposedApi(extensions));
let result: { [extensionId: string]: IExtensionDescription; } = {};
system.forEach((systemExtension) => {
result[systemExtension.id] = systemExtension;
});
user.forEach((userExtension) => {
if (result.hasOwnProperty(userExtension.id)) {
log.warn(userExtension.extensionLocation.fsPath, nls.localize('overwritingExtension', "Overwriting extension {0} with {1}.", result[userExtension.id].extensionLocation.fsPath, userExtension.extensionLocation.fsPath));
}
result[userExtension.id] = userExtension;
});
development.forEach(developedExtension => {
log.info('', nls.localize('extensionUnderDevelopment', "Loading development extension at {0}", developedExtension.extensionLocation.fsPath));
if (result.hasOwnProperty(developedExtension.id)) {
log.warn(developedExtension.extensionLocation.fsPath, nls.localize('overwritingExtension', "Overwriting extension {0} with {1}.", result[developedExtension.id].extensionLocation.fsPath, developedExtension.extensionLocation.fsPath));
}
result[developedExtension.id] = developedExtension;
});
return Object.keys(result).map(name => result[name]);
});
}
private _updateEnableProposedApi(extensions: IExtensionDescription[]): IExtensionDescription[] {
const enableProposedApiForAll = !this._environmentService.isBuilt || (!!this._environmentService.extensionDevelopmentPath && product.nameLong.indexOf('Insiders') >= 0);
const enableProposedApiFor = this._environmentService.args['enable-proposed-api'] || [];
for (const extension of extensions) {
if (!isFalsyOrEmpty(product.extensionAllowedProposedApi)
&& product.extensionAllowedProposedApi.indexOf(extension.id) >= 0
) {
// fast lane -> proposed api is available to all extensions
// that are listed in product.json-files
extension.enableProposedApi = true;
private _getRuntimeExtensions(allExtensions: IExtensionDescription[]): Promise<IExtensionDescription[]> {
return this._extensionEnablementService.getDisabledExtensions()
.then(disabledExtensions => {
} else if (extension.enableProposedApi && !extension.isBuiltin) {
if (
!enableProposedApiForAll &&
enableProposedApiFor.indexOf(extension.id) < 0
) {
extension.enableProposedApi = false;
console.error(`Extension '${extension.id} cannot use PROPOSED API (must started out of dev or enabled via --enable-proposed-api)`);
const result: { [extensionId: string]: IExtensionDescription; } = {};
const extensionsToDisable: IExtensionIdentifier[] = [];
const userMigratedSystemExtensions: IExtensionIdentifier[] = [{ id: BetterMergeId }];
} else {
// proposed api is available when developing or when an extension was explicitly
// spelled out via a command line argument
console.warn(`Extension '${extension.id}' uses PROPOSED API which is subject to change and removal without notice.`);
const enableProposedApiFor: string | string[] = this._environmentService.args['enable-proposed-api'] || [];
const enableProposedApiForAll = !this._environmentService.isBuilt ||
(!!this._environmentService.extensionDevelopmentPath && product.nameLong.indexOf('Insiders') >= 0) ||
(enableProposedApiFor.length === 0 && 'enable-proposed-api' in this._environmentService.args);
for (const extension of allExtensions) {
const isExtensionUnderDevelopment = this._environmentService.isExtensionDevelopment && extension.extensionLocation.scheme === Schemas.file && extension.extensionLocation.fsPath.indexOf(this._environmentService.extensionDevelopmentPath) === 0;
// Do not disable extensions under development
if (!isExtensionUnderDevelopment) {
if (disabledExtensions.some(disabled => areSameExtensions(disabled, extension))) {
continue;
}
}
if (!extension.isBuiltin) {
// Check if the extension is changed to system extension
const userMigratedSystemExtension = userMigratedSystemExtensions.filter(userMigratedSystemExtension => areSameExtensions(userMigratedSystemExtension, { id: extension.id }))[0];
if (userMigratedSystemExtension) {
extensionsToDisable.push(userMigratedSystemExtension);
continue;
}
}
result[extension.id] = this._updateEnableProposedApi(extension, enableProposedApiForAll, enableProposedApiFor);
}
const runtimeExtensions = Object.keys(result).map(name => result[name]);
this._telemetryService.publicLog('extensionsScanned', {
totalCount: runtimeExtensions.length,
disabledCount: disabledExtensions.length
});
if (extensionsToDisable.length) {
return this.extensionManagementService.getInstalled(LocalExtensionType.User)
.then(installed => {
const toDisable = installed.filter(i => extensionsToDisable.some(e => areSameExtensions({ id: getGalleryExtensionIdFromLocal(i) }, e)));
return TPromise.join(toDisable.map(e => this._extensionEnablementService.setEnablement(e, EnablementState.Disabled)));
})
.then(() => {
this._storageService.store(BetterMergeDisabledNowKey, true);
return runtimeExtensions;
});
} else {
return runtimeExtensions;
}
});
}
private _updateEnableProposedApi(extension: IExtensionDescription, enableProposedApiForAll: boolean, enableProposedApiFor: string | string[]): IExtensionDescription {
if (!isFalsyOrEmpty(product.extensionAllowedProposedApi)
&& product.extensionAllowedProposedApi.indexOf(extension.id) >= 0
) {
// 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.id) < 0
) {
extension.enableProposedApi = false;
console.error(`Extension '${extension.id} 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.id}' uses PROPOSED API which is subject to change and removal without notice.`);
}
}
return extensions;
return extension;
}
private _handleExtensionPointMessage(msg: IMessage) {
if (!this._extensionsMessages[msg.source]) {
this._extensionsMessages[msg.source] = [];
if (!this._extensionsMessages[msg.extensionId]) {
this._extensionsMessages[msg.extensionId] = [];
}
this._extensionsMessages[msg.source].push(msg);
this._extensionsMessages[msg.extensionId].push(msg);
if (msg.source === this._environmentService.extensionDevelopmentPath) {
const extension = this._registry.getExtensionDescription(msg.extensionId);
const strMsg = `[${msg.extensionId}]: ${msg.message}`;
if (extension && extension.isUnderDevelopment) {
// This message is about the extension currently being developed
this._showMessageToUser(msg.type, messageWithSource(msg));
this._showMessageToUser(msg.type, strMsg);
} else {
this._logMessageInConsole(msg.type, messageWithSource(msg));
this._logMessageInConsole(msg.type, strMsg);
}
if (!this._isDev && msg.extensionId) {
@@ -649,11 +654,11 @@ export class ExtensionService extends Disposable implements IExtensionService {
}
}
private static async _validateExtensionsCache(windowService: IWindowService, notificationService: INotificationService, environmentService: IEnvironmentService, cacheKey: string, input: ExtensionScannerInput): TPromise<void> {
private static async _validateExtensionsCache(windowService: IWindowService, notificationService: INotificationService, environmentService: IEnvironmentService, cacheKey: string, input: ExtensionScannerInput): Promise<void> {
const cacheFolder = path.join(environmentService.userDataPath, MANIFEST_CACHE_FOLDER);
const cacheFile = path.join(cacheFolder, cacheKey);
const expected = await ExtensionScanner.scanExtensions(input, new NullLogger());
const expected = JSON.parse(JSON.stringify(await ExtensionScanner.scanExtensions(input, new NullLogger())));
const cacheContents = await this._readExtensionCache(environmentService, cacheKey);
if (!cacheContents) {
@@ -684,7 +689,7 @@ export class ExtensionService extends Disposable implements IExtensionService {
);
}
private static async _readExtensionCache(environmentService: IEnvironmentService, cacheKey: string): TPromise<IExtensionCacheData> {
private static async _readExtensionCache(environmentService: IEnvironmentService, cacheKey: string): Promise<IExtensionCacheData> {
const cacheFolder = path.join(environmentService.userDataPath, MANIFEST_CACHE_FOLDER);
const cacheFile = path.join(cacheFolder, cacheKey);
@@ -698,7 +703,7 @@ export class ExtensionService extends Disposable implements IExtensionService {
return null;
}
private static async _writeExtensionCache(environmentService: IEnvironmentService, cacheKey: string, cacheContents: IExtensionCacheData): TPromise<void> {
private static async _writeExtensionCache(environmentService: IEnvironmentService, cacheKey: string, cacheContents: IExtensionCacheData): Promise<void> {
const cacheFolder = path.join(environmentService.userDataPath, MANIFEST_CACHE_FOLDER);
const cacheFile = path.join(cacheFolder, cacheKey);
@@ -715,7 +720,7 @@ export class ExtensionService extends Disposable implements IExtensionService {
}
}
private static async _scanExtensionsWithCache(windowService: IWindowService, notificationService: INotificationService, environmentService: IEnvironmentService, cacheKey: string, input: ExtensionScannerInput, log: ILog): TPromise<IExtensionDescription[]> {
private static async _scanExtensionsWithCache(windowService: IWindowService, notificationService: INotificationService, environmentService: IEnvironmentService, cacheKey: string, input: ExtensionScannerInput, log: ILog): Promise<IExtensionDescription[]> {
if (input.devMode) {
// Do not cache when running out of sources...
return ExtensionScanner.scanExtensions(input, log);
@@ -738,7 +743,11 @@ export class ExtensionService extends Disposable implements IExtensionService {
errors.onUnexpectedError(err);
}
}, 5000);
return cacheContents.result;
return cacheContents.result.map((extensionDescription) => {
// revive URI object
(<IRelaxedExtensionDescription>extensionDescription).extensionLocation = URI.revive(extensionDescription.extensionLocation);
return extensionDescription;
});
}
const counterLogger = new CounterLogger(log);
@@ -755,7 +764,7 @@ export class ExtensionService extends Disposable implements IExtensionService {
return result;
}
private static _scanInstalledExtensions(windowService: IWindowService, notificationService: INotificationService, environmentService: IEnvironmentService, log: ILog): TPromise<{ system: IExtensionDescription[], user: IExtensionDescription[], development: IExtensionDescription[] }> {
private static _scanInstalledExtensions(windowService: IWindowService, notificationService: INotificationService, environmentService: IEnvironmentService, extensionEnablementService: IExtensionEnablementService, log: ILog): TPromise<{ system: IExtensionDescription[], user: IExtensionDescription[], development: IExtensionDescription[] }> {
const translationConfig: TPromise<Translations> = platform.translationsConfigFile
? pfs.readFile(platform.translationsConfigFile, 'utf8').then((content) => {
@@ -780,11 +789,11 @@ export class ExtensionService extends Disposable implements IExtensionService {
notificationService,
environmentService,
BUILTIN_MANIFEST_CACHE_FILE,
new ExtensionScannerInput(version, commit, locale, devMode, getSystemExtensionsRoot(), true, translations),
new ExtensionScannerInput(version, commit, locale, devMode, getSystemExtensionsRoot(), true, false, translations),
log
);
let finalBuiltinExtensions: TPromise<IExtensionDescription[]> = builtinExtensions;
let finalBuiltinExtensions: TPromise<IExtensionDescription[]> = TPromise.wrap(builtinExtensions);
if (devMode) {
const builtInExtensionsFilePath = path.normalize(path.join(URI.parse(require.toUrl('')).fsPath, '..', 'build', 'builtInExtensions.json'));
@@ -795,7 +804,7 @@ export class ExtensionService extends Disposable implements IExtensionService {
const controlFile = pfs.readFile(controlFilePath, 'utf8')
.then<IBuiltInExtensionControl>(raw => JSON.parse(raw), () => ({} as any));
const input = new ExtensionScannerInput(version, commit, locale, devMode, getExtraDevSystemExtensionsRoot(), true, translations);
const input = new ExtensionScannerInput(version, commit, locale, devMode, getExtraDevSystemExtensionsRoot(), true, false, translations);
const extraBuiltinExtensions = TPromise.join([builtInExtensions, controlFile])
.then(([builtInExtensions, control]) => new ExtraBuiltInExtensionResolver(builtInExtensions, control))
.then(resolver => ExtensionScanner.scanExtensions(input, log, resolver));
@@ -812,8 +821,8 @@ export class ExtensionService extends Disposable implements IExtensionService {
let resultArr = Object.keys(resultMap).map((id) => resultMap[id]);
resultArr.sort((a, b) => {
const aLastSegment = path.basename(a.extensionFolderPath);
const bLastSegment = path.basename(b.extensionFolderPath);
const aLastSegment = path.basename(a.extensionLocation.fsPath);
const bLastSegment = path.basename(b.extensionLocation.fsPath);
if (aLastSegment < bLastSegment) {
return -1;
}
@@ -827,14 +836,14 @@ export class ExtensionService extends Disposable implements IExtensionService {
}
const userExtensions = (
environmentService.disableExtensions || !environmentService.extensionsPath
extensionEnablementService.allUserExtensionsDisabled || !environmentService.extensionsPath
? TPromise.as([])
: this._scanExtensionsWithCache(
windowService,
notificationService,
environmentService,
USER_MANIFEST_CACHE_FILE,
new ExtensionScannerInput(version, commit, locale, devMode, environmentService.extensionsPath, false, translations),
new ExtensionScannerInput(version, commit, locale, devMode, environmentService.extensionsPath, false, false, translations),
log
)
);
@@ -843,7 +852,7 @@ export class ExtensionService extends Disposable implements IExtensionService {
const developedExtensions = (
environmentService.isExtensionDevelopment
? ExtensionScanner.scanOneOrMultipleExtensions(
new ExtensionScannerInput(version, commit, locale, devMode, environmentService.extensionDevelopmentPath, false, translations), log
new ExtensionScannerInput(version, commit, locale, devMode, environmentService.extensionDevelopmentPath, false, true, translations), log
)
: TPromise.as([])
);
@@ -907,12 +916,15 @@ export class ExtensionService extends Disposable implements IExtensionService {
}
public _onExtensionActivated(extensionId: string, startup: boolean, codeLoadingTime: number, activateCallTime: number, activateResolvedTime: number, activationEvent: string): void {
this._extensionHostProcessManager.onExtensionActivated(extensionId, startup, codeLoadingTime, activateCallTime, activateResolvedTime, activationEvent);
this._extensionHostProcessActivationTimes[extensionId] = new ActivationTimes(startup, codeLoadingTime, activateCallTime, activateResolvedTime, activationEvent);
this._onDidChangeExtensionsStatus.fire([extensionId]);
}
public _onExtensionRuntimeError(extensionId: string, err: Error): void {
this._extensionHostProcessManager.onExtensionRuntimeError(extensionId, err);
if (!this._extensionHostExtensionRuntimeErrors[extensionId]) {
this._extensionHostExtensionRuntimeErrors[extensionId] = [];
}
this._extensionHostExtensionRuntimeErrors[extensionId].push(err);
this._onDidChangeExtensionsStatus.fire([extensionId]);
}
@@ -923,7 +935,6 @@ export class ExtensionService extends Disposable implements IExtensionService {
this._extensionsMessages[extensionId].push({
type: severity,
message: message,
source: null,
extensionId: null,
extensionPointId: null
});

View File

@@ -0,0 +1,51 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IExtensionManagementService, IExtensionManagementServerService, IExtensionManagementServer } from 'vs/platform/extensionManagement/common/extensionManagement';
import URI from 'vs/base/common/uri';
import { Schemas } from 'vs/base/common/network';
export class ExtensionManagementServerService implements IExtensionManagementServerService {
_serviceBrand: any;
readonly extensionManagementServers: IExtensionManagementServer[];
constructor(
localExtensionManagementService: IExtensionManagementService
) {
this.extensionManagementServers = [{ extensionManagementService: localExtensionManagementService, location: URI.from({ scheme: Schemas.file }) }];
}
getExtensionManagementServer(location: URI): IExtensionManagementServer {
return this.extensionManagementServers[0];
}
getDefaultExtensionManagementServer(): IExtensionManagementServer {
return this.extensionManagementServers[0];
}
}
export class SingleServerExtensionManagementServerService implements IExtensionManagementServerService {
_serviceBrand: any;
readonly extensionManagementServers: IExtensionManagementServer[];
constructor(
extensionManagementServer: IExtensionManagementServer
) {
this.extensionManagementServers = [extensionManagementServer];
}
getExtensionManagementServer(location: URI): IExtensionManagementServer {
location = location.scheme === Schemas.file ? URI.from({ scheme: Schemas.file }) : location;
return this.extensionManagementServers.filter(server => location.authority === server.location.authority)[0];
}
getDefaultExtensionManagementServer(): IExtensionManagementServer {
return this.extensionManagementServers[0];
}
}

View File

@@ -17,6 +17,7 @@ import * as semver from 'semver';
import { getIdAndVersionFromLocalExtensionId } from 'vs/platform/extensionManagement/node/extensionManagementUtil';
import { getParseErrorMessage } from 'vs/base/common/jsonErrorMessages';
import { groupByExtension, getGalleryExtensionId, getLocalExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import URI from 'vs/base/common/uri';
const MANIFEST_FILE = 'package.json';
@@ -67,13 +68,15 @@ abstract class ExtensionManifestHandler {
protected readonly _log: ILog;
protected readonly _absoluteFolderPath: string;
protected readonly _isBuiltin: boolean;
protected readonly _isUnderDevelopment: boolean;
protected readonly _absoluteManifestPath: string;
constructor(ourVersion: string, log: ILog, absoluteFolderPath: string, isBuiltin: boolean) {
constructor(ourVersion: string, log: ILog, absoluteFolderPath: string, isBuiltin: boolean, isUnderDevelopment: boolean) {
this._ourVersion = ourVersion;
this._log = log;
this._absoluteFolderPath = absoluteFolderPath;
this._isBuiltin = isBuiltin;
this._isUnderDevelopment = isUnderDevelopment;
this._absoluteManifestPath = join(absoluteFolderPath, MANIFEST_FILE);
}
}
@@ -112,8 +115,8 @@ class ExtensionManifestNLSReplacer extends ExtensionManifestHandler {
private readonly _nlsConfig: NlsConfiguration;
constructor(ourVersion: string, log: ILog, absoluteFolderPath: string, isBuiltin: boolean, nlsConfig: NlsConfiguration) {
super(ourVersion, log, absoluteFolderPath, isBuiltin);
constructor(ourVersion: string, log: ILog, absoluteFolderPath: string, isBuiltin: boolean, isUnderDevelopment: boolean, nlsConfig: NlsConfiguration) {
super(ourVersion, log, absoluteFolderPath, isBuiltin, isUnderDevelopment);
this._nlsConfig = nlsConfig;
}
@@ -209,7 +212,7 @@ class ExtensionManifestNLSReplacer extends ExtensionManifestHandler {
* Parses original message bundle, returns null if the original message bundle is null.
*/
private static resolveOriginalMessageBundle(originalMessageBundle: string, errors: json.ParseError[]) {
return new TPromise<{ [key: string]: string; }>((c, e, p) => {
return new TPromise<{ [key: string]: string; }>((c, e) => {
if (originalMessageBundle) {
pfs.readFile(originalMessageBundle).then(originalBundleContent => {
c(json.parse(originalBundleContent.toString(), errors));
@@ -227,7 +230,7 @@ class ExtensionManifestNLSReplacer extends ExtensionManifestHandler {
* If the localized file is not present, returns null for the original and marks original as localized.
*/
private static findMessageBundles(nlsConfig: NlsConfiguration, basename: string): TPromise<{ localized: string, original: string }> {
return new TPromise<{ localized: string, original: string }>((c, e, p) => {
return new TPromise<{ localized: string, original: string }>((c, e) => {
function loop(basename: string, locale: string): void {
let toCheck = `${basename}.nls.${locale}.json`;
pfs.fileExists(toCheck).then(exists => {
@@ -300,24 +303,27 @@ class ExtensionManifestNLSReplacer extends ExtensionManifestHandler {
}
}
// Relax the readonly properties here, it is the one place where we check and normalize values
export interface IRelaxedExtensionDescription {
id: string;
name: string;
version: string;
publisher: string;
isBuiltin: boolean;
isUnderDevelopment: boolean;
extensionLocation: URI;
engines: {
vscode: string;
};
main?: string;
enableProposedApi?: boolean;
}
class ExtensionManifestValidator extends ExtensionManifestHandler {
validate(_extensionDescription: IExtensionDescription): IExtensionDescription {
// Relax the readonly properties here, it is the one place where we check and normalize values
interface IRelaxedExtensionDescription {
id: string;
name: string;
version: string;
publisher: string;
isBuiltin: boolean;
extensionFolderPath: string;
engines: {
vscode: string;
};
main?: string;
enableProposedApi?: boolean;
}
let extensionDescription = <IRelaxedExtensionDescription>_extensionDescription;
extensionDescription.isBuiltin = this._isBuiltin;
extensionDescription.isUnderDevelopment = this._isUnderDevelopment;
let notices: string[] = [];
if (!ExtensionManifestValidator.isValidExtensionDescription(this._ourVersion, this._absoluteFolderPath, extensionDescription, notices)) {
@@ -340,7 +346,7 @@ class ExtensionManifestValidator extends ExtensionManifestHandler {
extensionDescription.main = join(this._absoluteFolderPath, extensionDescription.main);
}
extensionDescription.extensionFolderPath = this._absoluteFolderPath;
extensionDescription.extensionLocation = URI.file(this._absoluteFolderPath);
return extensionDescription;
}
@@ -444,6 +450,7 @@ export class ExtensionScannerInput {
public readonly devMode: boolean,
public readonly absoluteFolderPath: string,
public readonly isBuiltin: boolean,
public readonly isUnderDevelopment: boolean,
public readonly tanslations: Translations
) {
// Keep empty!! (JSON.parse)
@@ -466,6 +473,7 @@ export class ExtensionScannerInput {
&& a.devMode === b.devMode
&& a.absoluteFolderPath === b.absoluteFolderPath
&& a.isBuiltin === b.isBuiltin
&& a.isUnderDevelopment === b.isUnderDevelopment
&& a.mtime === b.mtime
&& Translations.equals(a.tanslations, b.tanslations)
);
@@ -496,23 +504,23 @@ export class ExtensionScanner {
/**
* Read the extension defined in `absoluteFolderPath`
*/
public static scanExtension(version: string, log: ILog, absoluteFolderPath: string, isBuiltin: boolean, nlsConfig: NlsConfiguration): TPromise<IExtensionDescription> {
public static scanExtension(version: string, log: ILog, absoluteFolderPath: string, isBuiltin: boolean, isUnderDevelopment: boolean, nlsConfig: NlsConfiguration): TPromise<IExtensionDescription> {
absoluteFolderPath = normalize(absoluteFolderPath);
let parser = new ExtensionManifestParser(version, log, absoluteFolderPath, isBuiltin);
let parser = new ExtensionManifestParser(version, log, absoluteFolderPath, isBuiltin, isUnderDevelopment);
return parser.parse().then((extensionDescription) => {
if (extensionDescription === null) {
return null;
}
let nlsReplacer = new ExtensionManifestNLSReplacer(version, log, absoluteFolderPath, isBuiltin, nlsConfig);
let nlsReplacer = new ExtensionManifestNLSReplacer(version, log, absoluteFolderPath, isBuiltin, isUnderDevelopment, nlsConfig);
return nlsReplacer.replaceNLS(extensionDescription);
}).then((extensionDescription) => {
if (extensionDescription === null) {
return null;
}
let validator = new ExtensionManifestValidator(version, log, absoluteFolderPath, isBuiltin);
let validator = new ExtensionManifestValidator(version, log, absoluteFolderPath, isBuiltin, isUnderDevelopment);
return validator.validate(extensionDescription);
});
}
@@ -520,9 +528,10 @@ export class ExtensionScanner {
/**
* Scan a list of extensions defined in `absoluteFolderPath`
*/
public static async scanExtensions(input: ExtensionScannerInput, log: ILog, resolver: IExtensionResolver = null): TPromise<IExtensionDescription[]> {
public static async scanExtensions(input: ExtensionScannerInput, log: ILog, resolver: IExtensionResolver = null): Promise<IExtensionDescription[]> {
const absoluteFolderPath = input.absoluteFolderPath;
const isBuiltin = input.isBuiltin;
const isUnderDevelopment = input.isUnderDevelopment;
if (!resolver) {
resolver = new DefaultExtensionResolver(absoluteFolderPath);
@@ -563,7 +572,7 @@ export class ExtensionScanner {
}
const nlsConfig = ExtensionScannerInput.createNLSConfig(input);
let extensionDescriptions = await TPromise.join(refs.map(r => this.scanExtension(input.ourVersion, log, r.path, isBuiltin, nlsConfig)));
let extensionDescriptions = await TPromise.join(refs.map(r => this.scanExtension(input.ourVersion, log, r.path, isBuiltin, isUnderDevelopment, nlsConfig)));
extensionDescriptions = extensionDescriptions.filter(item => item !== null && !obsolete[getLocalExtensionId(getGalleryExtensionId(item.publisher, item.name), item.version)]);
if (!isBuiltin) {
@@ -573,7 +582,7 @@ export class ExtensionScanner {
}
extensionDescriptions.sort((a, b) => {
if (a.extensionFolderPath < b.extensionFolderPath) {
if (a.extensionLocation.fsPath < b.extensionLocation.fsPath) {
return -1;
}
return 1;
@@ -592,11 +601,12 @@ export class ExtensionScanner {
public static scanOneOrMultipleExtensions(input: ExtensionScannerInput, log: ILog): TPromise<IExtensionDescription[]> {
const absoluteFolderPath = input.absoluteFolderPath;
const isBuiltin = input.isBuiltin;
const isUnderDevelopment = input.isUnderDevelopment;
return pfs.fileExists(join(absoluteFolderPath, MANIFEST_FILE)).then((exists) => {
if (exists) {
const nlsConfig = ExtensionScannerInput.createNLSConfig(input);
return this.scanExtension(input.ourVersion, log, absoluteFolderPath, isBuiltin, nlsConfig).then((extensionDescription) => {
return this.scanExtension(input.ourVersion, log, absoluteFolderPath, isBuiltin, isUnderDevelopment, nlsConfig).then((extensionDescription) => {
if (extensionDescription === null) {
return [];
}
@@ -609,4 +619,4 @@ export class ExtensionScanner {
return [];
});
}
}
}

View File

@@ -10,16 +10,12 @@ import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc';
import { LazyPromise } from 'vs/workbench/services/extensions/node/lazyPromise';
import { ProxyIdentifier, IRPCProtocol } from 'vs/workbench/services/extensions/node/proxyIdentifier';
import { CharCode } from 'vs/base/common/charCode';
import URI, { UriComponents } from 'vs/base/common/uri';
import URI from 'vs/base/common/uri';
import { MarshalledObject } from 'vs/base/common/marshalling';
import { IURITransformer } from 'vs/base/common/uriIpc';
declare var Proxy: any; // TODO@TypeScript
export interface IURITransformer {
transformIncoming(uri: UriComponents): UriComponents;
transformOutgoing(uri: URI): URI;
}
function _transformOutgoingURIs(obj: any, transformer: IURITransformer, depth: number): any {
if (!obj || depth > 200) {
@@ -45,7 +41,7 @@ function _transformOutgoingURIs(obj: any, transformer: IURITransformer, depth: n
return null;
}
function transformOutgoingURIs(obj: any, transformer: IURITransformer): any {
export function transformOutgoingURIs(obj: any, transformer: IURITransformer): any {
const result = _transformOutgoingURIs(obj, transformer, 0);
if (result === null) {
// no change
@@ -137,7 +133,7 @@ export class RPCProtocol implements IRPCProtocol {
private _createProxy<T>(proxyId: string): T {
let handler = {
get: (target, name: string) => {
get: (target: any, name: string) => {
if (!target[name] && name.charCodeAt(0) === CharCode.DollarSign) {
target[name] = (...myArgs: any[]) => {
return this._remoteCall(proxyId, name, myArgs);

View File

@@ -14,7 +14,7 @@ import { join, extname } from 'path';
import { ITextResourceConfigurationService } from 'vs/editor/common/services/resourceConfiguration';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { Disposable } from 'vs/base/common/lifecycle';
export interface IEncodingOverride {
parent?: uri;
@@ -26,9 +26,8 @@ export interface IEncodingOverride {
// service and then ideally be passed in as option to the file service
// the file service should talk about string | Buffer for reading and writing and only convert
// to strings if a encoding is provided
export class ResourceEncodings implements IResourceEncodings {
export class ResourceEncodings extends Disposable implements IResourceEncodings {
private encodingOverride: IEncodingOverride[];
private toDispose: IDisposable[];
constructor(
private textResourceConfigurationService: ITextResourceConfigurationService,
@@ -36,8 +35,9 @@ export class ResourceEncodings implements IResourceEncodings {
private contextService: IWorkspaceContextService,
encodingOverride?: IEncodingOverride[]
) {
super();
this.encodingOverride = encodingOverride || this.getEncodingOverrides();
this.toDispose = [];
this.registerListeners();
}
@@ -45,12 +45,12 @@ export class ResourceEncodings implements IResourceEncodings {
private registerListeners(): void {
// Workspace Folder Change
this.toDispose.push(this.contextService.onDidChangeWorkspaceFolders(() => {
this._register(this.contextService.onDidChangeWorkspaceFolders(() => {
this.encodingOverride = this.getEncodingOverrides();
}));
}
public getReadEncoding(resource: uri, options: IResolveContentOptions, detected: encoding.IDetectedEncodingResult): string {
getReadEncoding(resource: uri, options: IResolveContentOptions, detected: encoding.IDetectedEncodingResult): string {
let preferredEncoding: string;
// Encoding passed in as option
@@ -79,7 +79,7 @@ export class ResourceEncodings implements IResourceEncodings {
return this.getEncodingForResource(resource, preferredEncoding);
}
public getWriteEncoding(resource: uri, preferredEncoding?: string): string {
getWriteEncoding(resource: uri, preferredEncoding?: string): string {
return this.getEncodingForResource(resource, preferredEncoding);
}
@@ -139,8 +139,4 @@ export class ResourceEncodings implements IResourceEncodings {
return null;
}
public dispose(): void {
this.toDispose = dispose(this.toDispose);
}
}

View File

@@ -18,11 +18,11 @@ import * as arrays from 'vs/base/common/arrays';
import { TPromise } from 'vs/base/common/winjs.base';
import * as objects from 'vs/base/common/objects';
import * as extfs from 'vs/base/node/extfs';
import { nfcall, ThrottledDelayer, asWinJsPromise } from 'vs/base/common/async';
import { nfcall, ThrottledDelayer, toWinJsPromise } from 'vs/base/common/async';
import uri from 'vs/base/common/uri';
import * as nls from 'vs/nls';
import { isWindows, isLinux, isMacintosh } from 'vs/base/common/platform';
import { dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { IDisposable, toDisposable, Disposable } from 'vs/base/common/lifecycle';
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
import * as pfs from 'vs/base/node/pfs';
import * as encoding from 'vs/base/node/encoding';
@@ -76,9 +76,9 @@ export interface IFileServiceTestOptions {
encodingOverride?: IEncodingOverride[];
}
export class FileService implements IFileService {
export class FileService extends Disposable implements IFileService {
public _serviceBrand: any;
_serviceBrand: any;
private static readonly FS_EVENT_DELAY = 50; // aggregate and only emit events when changes have stopped for this duration (in ms)
private static readonly FS_REWATCH_DELAY = 300; // delay to rewatch a file that was renamed or deleted (in ms)
@@ -89,11 +89,14 @@ export class FileService implements IFileService {
private static readonly ENOSPC_ERROR = 'ENOSPC';
private static readonly ENOSPC_ERROR_IGNORE_KEY = 'ignoreEnospcError';
protected readonly _onFileChanges: Emitter<FileChangesEvent>;
protected readonly _onAfterOperation: Emitter<FileOperationEvent>;
protected readonly _onDidChangeFileSystemProviderRegistrations = new Emitter<IFileSystemProviderRegistrationEvent>();
protected readonly _onFileChanges: Emitter<FileChangesEvent> = this._register(new Emitter<FileChangesEvent>());
get onFileChanges(): Event<FileChangesEvent> { return this._onFileChanges.event; }
protected toDispose: IDisposable[];
protected readonly _onAfterOperation: Emitter<FileOperationEvent> = this._register(new Emitter<FileOperationEvent>());
get onAfterOperation(): Event<FileOperationEvent> { return this._onAfterOperation.event; }
protected readonly _onDidChangeFileSystemProviderRegistrations = this._register(new Emitter<IFileSystemProviderRegistrationEvent>());
get onDidChangeFileSystemProviderRegistrations(): Event<IFileSystemProviderRegistrationEvent> { return this._onDidChangeFileSystemProviderRegistrations.event; }
private activeWorkspaceFileChangeWatcher: IDisposable;
private activeFileChangesWatchers: ResourceMap<fs.FSWatcher>;
@@ -112,13 +115,7 @@ export class FileService implements IFileService {
private notificationService: INotificationService,
private options: IFileServiceTestOptions = Object.create(null)
) {
this.toDispose = [];
this._onFileChanges = new Emitter<FileChangesEvent>();
this.toDispose.push(this._onFileChanges);
this._onAfterOperation = new Emitter<FileOperationEvent>();
this.toDispose.push(this._onAfterOperation);
super();
this.activeFileChangesWatchers = new ResourceMap<fs.FSWatcher>();
this.fileChangesWatchDelayer = new ThrottledDelayer<void>(FileService.FS_EVENT_DELAY);
@@ -129,7 +126,7 @@ export class FileService implements IFileService {
this.registerListeners();
}
public get encoding(): ResourceEncodings {
get encoding(): ResourceEncodings {
return this._encoding;
}
@@ -141,7 +138,7 @@ export class FileService implements IFileService {
});
// Workbench State Change
this.toDispose.push(this.contextService.onDidChangeWorkbenchState(() => {
this._register(this.contextService.onDidChangeWorkbenchState(() => {
if (this.lifecycleService.phase >= LifecyclePhase.Running) {
this.setupFileWatching();
}
@@ -195,14 +192,6 @@ export class FileService implements IFileService {
}
}
public get onFileChanges(): Event<FileChangesEvent> {
return this._onFileChanges.event;
}
public get onAfterOperation(): Event<FileOperationEvent> {
return this._onAfterOperation.event;
}
private setupFileWatching(): void {
// dispose old if any
@@ -234,36 +223,34 @@ export class FileService implements IFileService {
const legacyWindowsWatcher = new WindowsWatcherService(this.contextService, watcherIgnoredPatterns, e => this._onFileChanges.fire(e), err => this.handleError(err), this.environmentService.verbose);
this.activeWorkspaceFileChangeWatcher = toDisposable(legacyWindowsWatcher.startWatching());
} else {
const legacyUnixWatcher = new UnixWatcherService(this.contextService, watcherIgnoredPatterns, e => this._onFileChanges.fire(e), err => this.handleError(err), this.environmentService.verbose);
const legacyUnixWatcher = new UnixWatcherService(this.contextService, this.configurationService, e => this._onFileChanges.fire(e), err => this.handleError(err), this.environmentService.verbose);
this.activeWorkspaceFileChangeWatcher = toDisposable(legacyUnixWatcher.startWatching());
}
}
}
public readonly onDidChangeFileSystemProviderRegistrations: Event<IFileSystemProviderRegistrationEvent> = this._onDidChangeFileSystemProviderRegistrations.event;
public registerProvider(scheme: string, provider: IFileSystemProvider): IDisposable {
registerProvider(scheme: string, provider: IFileSystemProvider): IDisposable {
throw new Error('not implemented');
}
public canHandleResource(resource: uri): boolean {
canHandleResource(resource: uri): boolean {
return resource.scheme === Schemas.file;
}
public resolveFile(resource: uri, options?: IResolveFileOptions): TPromise<IFileStat> {
resolveFile(resource: uri, options?: IResolveFileOptions): TPromise<IFileStat> {
return this.resolve(resource, options);
}
public resolveFiles(toResolve: { resource: uri, options?: IResolveFileOptions }[]): TPromise<IResolveFileResult[]> {
resolveFiles(toResolve: { resource: uri, options?: IResolveFileOptions }[]): TPromise<IResolveFileResult[]> {
return TPromise.join(toResolve.map(resourceAndOptions => this.resolve(resourceAndOptions.resource, resourceAndOptions.options)
.then(stat => ({ stat, success: true }), error => ({ stat: void 0, success: false }))));
}
public existsFile(resource: uri): TPromise<boolean> {
existsFile(resource: uri): TPromise<boolean> {
return this.resolveFile(resource).then(() => true, () => false);
}
public resolveContent(resource: uri, options?: IResolveContentOptions): TPromise<IContent> {
resolveContent(resource: uri, options?: IResolveContentOptions): TPromise<IContent> {
return this.resolveStreamContent(resource, options).then(streamContent => {
return new TPromise<IContent>((resolve, reject) => {
@@ -273,6 +260,7 @@ export class FileService implements IFileService {
mtime: streamContent.mtime,
etag: streamContent.etag,
encoding: streamContent.encoding,
isReadonly: streamContent.isReadonly,
value: ''
};
@@ -285,7 +273,7 @@ export class FileService implements IFileService {
});
}
public resolveStreamContent(resource: uri, options?: IResolveContentOptions): TPromise<IStreamContent> {
resolveStreamContent(resource: uri, options?: IResolveContentOptions): TPromise<IStreamContent> {
// Guard early against attempts to resolve an invalid file path
if (resource.scheme !== Schemas.file || !resource.fsPath) {
@@ -302,6 +290,7 @@ export class FileService implements IFileService {
mtime: void 0,
etag: void 0,
encoding: void 0,
isReadonly: false,
value: void 0
};
@@ -374,7 +363,7 @@ export class FileService implements IFileService {
return onStatError(err);
});
let completePromise: Thenable<any>;
let completePromise: TPromise<void>;
// await the stat iff we already have an etag so that we compare the
// etag from the stat before we actually read the file again.
@@ -387,24 +376,45 @@ export class FileService implements IFileService {
// a fresh load without a previous etag which means we can resolve the file stat
// and the content at the same time, avoiding the waterfall.
else {
completePromise = Promise.all([statsPromise, this.fillInContents(result, resource, options, contentResolverTokenSource.token)]);
completePromise = TPromise.join([statsPromise, this.fillInContents(result, resource, options, contentResolverTokenSource.token)]).then(() => void 0, error => {
// Joining promises via TPromise will execute both and return errors
// as array-like object for each. Since each can return a FileOperationError
// we want to prefer that one if possible. Otherwise we just return with the
// first error we get.
const firstError = error[0];
const secondError = error[1];
if (FileOperationError.isFileOperationError(firstError)) {
return TPromise.wrapError(firstError);
}
if (FileOperationError.isFileOperationError(secondError)) {
return TPromise.wrapError(secondError);
}
return TPromise.wrapError(firstError || secondError);
});
}
return TPromise.wrap(completePromise).then(() => {
return completePromise.then(() => {
contentResolverTokenSource.dispose();
return result;
}, error => {
contentResolverTokenSource.dispose();
return TPromise.wrapError(error);
});
}
private fillInContents(content: IStreamContent, resource: uri, options: IResolveContentOptions, token: CancellationToken): Thenable<any> {
private fillInContents(content: IStreamContent, resource: uri, options: IResolveContentOptions, token: CancellationToken): TPromise<void> {
return this.resolveFileData(resource, options, token).then(data => {
content.encoding = data.encoding;
content.value = data.stream;
});
}
private resolveFileData(resource: uri, options: IResolveContentOptions, token: CancellationToken): Thenable<IContentData> {
private resolveFileData(resource: uri, options: IResolveContentOptions, token: CancellationToken): TPromise<IContentData> {
const chunkBuffer = BufferPool._64K.acquire();
@@ -413,7 +423,7 @@ export class FileService implements IFileService {
stream: void 0
};
return new Promise<IContentData>((resolve, reject) => {
return new TPromise<IContentData>((resolve, reject) => {
fs.open(this.toAbsolutePath(resource), 'r', (err, fd) => {
if (err) {
if (err.code === 'ENOENT') {
@@ -554,7 +564,7 @@ export class FileService implements IFileService {
});
}
public updateContent(resource: uri, value: string | ITextSnapshot, options: IUpdateContentOptions = Object.create(null)): TPromise<IFileStat> {
updateContent(resource: uri, value: string | ITextSnapshot, options: IUpdateContentOptions = Object.create(null)): TPromise<IFileStat> {
if (options.writeElevated) {
return this.doUpdateContentElevated(resource, value, options);
}
@@ -668,7 +678,7 @@ export class FileService implements IFileService {
return this.updateContent(uri.file(tmpPath), value, writeOptions).then(() => {
// 3.) invoke our CLI as super user
return (import('sudo-prompt')).then(sudoPrompt => {
return toWinJsPromise(import('sudo-prompt')).then(sudoPrompt => {
return new TPromise<void>((c, e) => {
const promptOptions = {
name: this.environmentService.appNameLong.replace('-', ''),
@@ -716,7 +726,7 @@ export class FileService implements IFileService {
});
}
public createFile(resource: uri, content: string = '', options: ICreateFileOptions = Object.create(null)): TPromise<IFileStat> {
createFile(resource: uri, content: string = '', options: ICreateFileOptions = Object.create(null)): TPromise<IFileStat> {
const absolutePath = this.toAbsolutePath(resource);
let checkFilePromise: TPromise<boolean>;
@@ -747,7 +757,7 @@ export class FileService implements IFileService {
});
}
public createFolder(resource: uri): TPromise<IFileStat> {
createFolder(resource: uri): TPromise<IFileStat> {
// 1.) Create folder
const absolutePath = this.toAbsolutePath(resource);
@@ -826,17 +836,11 @@ export class FileService implements IFileService {
));
}
public rename(resource: uri, newName: string): TPromise<IFileStat> {
const newPath = paths.join(paths.dirname(resource.fsPath), newName);
return this.moveFile(resource, uri.file(newPath));
}
public moveFile(source: uri, target: uri, overwrite?: boolean): TPromise<IFileStat> {
moveFile(source: uri, target: uri, overwrite?: boolean): TPromise<IFileStat> {
return this.moveOrCopyFile(source, target, false, overwrite);
}
public copyFile(source: uri, target: uri, overwrite?: boolean): TPromise<IFileStat> {
copyFile(source: uri, target: uri, overwrite?: boolean): TPromise<IFileStat> {
return this.moveOrCopyFile(source, target, true, overwrite);
}
@@ -882,7 +886,7 @@ export class FileService implements IFileService {
return TPromise.wrapError<boolean>(new Error(nls.localize('unableToMoveCopyError', "Unable to move/copy. File would replace folder it is contained in."))); // catch this corner case!
}
deleteTargetPromise = this.del(uri.file(targetPath));
deleteTargetPromise = this.del(uri.file(targetPath), { recursive: true });
}
return deleteTargetPromise.then(() => {
@@ -903,36 +907,56 @@ export class FileService implements IFileService {
});
}
public del(resource: uri, useTrash?: boolean): TPromise<void> {
if (useTrash) {
return asWinJsPromise(() => this.doMoveItemToTrash(resource));
del(resource: uri, options?: { useTrash?: boolean, recursive?: boolean }): TPromise<void> {
if (options && options.useTrash) {
return this.doMoveItemToTrash(resource);
}
return this.doDelete(resource);
return this.doDelete(resource, options && options.recursive);
}
private doMoveItemToTrash(resource: uri): Promise<void> {
private doMoveItemToTrash(resource: uri): TPromise<void> {
const absolutePath = resource.fsPath;
return (import('electron')).then(electron => { // workaround for https://github.com/Microsoft/vscode/issues/48205
const result = electron.shell.moveItemToTrash(absolutePath);
if (!result) {
return TPromise.wrapError<void>(new Error(isWindows ? nls.localize('binFailed', "Failed to move '{0}' to the recycle bin", paths.basename(absolutePath)) : nls.localize('trashFailed', "Failed to move '{0}' to the trash", paths.basename(absolutePath))));
}
const shell = (require('electron') as Electron.RendererInterface).shell; // workaround for being able to run tests out of VSCode debugger
const result = shell.moveItemToTrash(absolutePath);
if (!result) {
return TPromise.wrapError(new Error(isWindows ? nls.localize('binFailed', "Failed to move '{0}' to the recycle bin", paths.basename(absolutePath)) : nls.localize('trashFailed', "Failed to move '{0}' to the trash", paths.basename(absolutePath))));
}
this._onAfterOperation.fire(new FileOperationEvent(resource, FileOperation.DELETE));
this._onAfterOperation.fire(new FileOperationEvent(resource, FileOperation.DELETE));
return TPromise.wrap(null);
});
return TPromise.as(void 0);
}
private doDelete(resource: uri): TPromise<void> {
private doDelete(resource: uri, recursive: boolean): TPromise<void> {
const absolutePath = this.toAbsolutePath(resource);
return pfs.del(absolutePath, os.tmpdir()).then(() => {
let assertNonRecursiveDelete: TPromise<void>;
if (!recursive) {
assertNonRecursiveDelete = pfs.stat(absolutePath).then(stat => {
if (!stat.isDirectory()) {
return TPromise.as(void 0);
}
// Events
this._onAfterOperation.fire(new FileOperationEvent(resource, FileOperation.DELETE));
return pfs.readdir(absolutePath).then(children => {
if (children.length === 0) {
return TPromise.as(void 0);
}
return TPromise.wrapError(new Error(nls.localize('deleteFailed', "Failed to delete non-empty folder '{0}'.", paths.basename(absolutePath))));
});
}, error => TPromise.as(void 0) /* ignore errors */);
} else {
assertNonRecursiveDelete = TPromise.as(void 0);
}
return assertNonRecursiveDelete.then(() => {
return pfs.del(absolutePath, os.tmpdir()).then(() => {
// Events
this._onAfterOperation.fire(new FileOperationEvent(resource, FileOperation.DELETE));
});
});
}
@@ -963,7 +987,7 @@ export class FileService implements IFileService {
});
}
public watchFileChanges(resource: uri): void {
watchFileChanges(resource: uri): void {
assert.ok(resource && resource.scheme === Schemas.file, `Invalid resource for watching: ${resource}`);
// Create or get watcher for provided path
@@ -1052,7 +1076,7 @@ export class FileService implements IFileService {
});
}
public unwatchFileChanges(resource: uri): void {
unwatchFileChanges(resource: uri): void {
const watcher = this.activeFileChangesWatchers.get(resource);
if (watcher) {
watcher.close();
@@ -1060,8 +1084,8 @@ export class FileService implements IFileService {
}
}
public dispose(): void {
this.toDispose = dispose(this.toDispose);
dispose(): void {
super.dispose();
if (this.activeWorkspaceFileChangeWatcher) {
this.activeWorkspaceFileChangeWatcher.dispose();
@@ -1107,13 +1131,14 @@ export class StatResolver {
this.etag = etag(size, mtime);
}
public resolve(options: IResolveFileOptions): TPromise<IFileStat> {
resolve(options: IResolveFileOptions): TPromise<IFileStat> {
// General Data
const fileStat: IFileStat = {
resource: this.resource,
isDirectory: this.isDirectory,
isSymbolicLink: this.isSymbolicLink,
isReadonly: false,
name: this.name,
etag: this.etag,
size: this.size,
@@ -1198,6 +1223,7 @@ export class StatResolver {
resource: fileResource,
isDirectory: fileStat.isDirectory(),
isSymbolicLink,
isReadonly: false,
name: file,
mtime: fileStat.mtime.getTime(),
etag: etag(fileStat),

View File

@@ -6,7 +6,7 @@
import { posix } from 'path';
import { flatten, isFalsyOrEmpty } from 'vs/base/common/arrays';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { IDisposable, dispose, Disposable } from 'vs/base/common/lifecycle';
import { TernarySearchTree, keys } from 'vs/base/common/map';
import { Schemas } from 'vs/base/common/network';
import URI from 'vs/base/common/uri';
@@ -45,6 +45,7 @@ function toIFileStat(provider: IFileSystemProvider, tuple: [URI, IStat], recurse
name: posix.basename(resource.path),
isDirectory: (stat.type & FileType.Directory) !== 0,
isSymbolicLink: (stat.type & FileType.SymbolicLink) !== 0,
isReadonly: !!(provider.capabilities & FileSystemProviderCapabilities.Readonly),
mtime: stat.mtime,
size: stat.size,
etag: stat.mtime.toString(29) + stat.size.toString(31),
@@ -85,9 +86,8 @@ export function toDeepIFileStat(provider: IFileSystemProvider, tuple: [URI, ISta
});
}
class WorkspaceWatchLogic {
class WorkspaceWatchLogic extends Disposable {
private _disposables: IDisposable[] = [];
private _watches = new Map<string, URI>();
constructor(
@@ -95,9 +95,11 @@ class WorkspaceWatchLogic {
@IConfigurationService private _configurationService: IConfigurationService,
@IWorkspaceContextService private _contextService: IWorkspaceContextService,
) {
super();
this._refresh();
this._disposables.push(this._contextService.onDidChangeWorkspaceFolders(e => {
this._register(this._contextService.onDidChangeWorkspaceFolders(e => {
for (const removed of e.removed) {
this._unwatchWorkspace(removed.uri);
}
@@ -105,10 +107,10 @@ class WorkspaceWatchLogic {
this._watchWorkspace(added.uri);
}
}));
this._disposables.push(this._contextService.onDidChangeWorkbenchState(e => {
this._register(this._contextService.onDidChangeWorkbenchState(e => {
this._refresh();
}));
this._disposables.push(this._configurationService.onDidChangeConfiguration(e => {
this._register(this._configurationService.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('files.watcherExclude')) {
this._refresh();
}
@@ -117,7 +119,7 @@ class WorkspaceWatchLogic {
dispose(): void {
this._unwatchWorkspaces();
this._disposables = dispose(this._disposables);
super.dispose();
}
private _refresh(): void {
@@ -183,7 +185,7 @@ export class RemoteFileService extends FileService {
this._provider = new Map<string, IFileSystemProvider>();
this._lastKnownSchemes = JSON.parse(this._storageService.get('remote_schemes', undefined, '[]'));
this.toDispose.push(new WorkspaceWatchLogic(this, configurationService, contextService));
this._register(new WorkspaceWatchLogic(this, configurationService, contextService));
}
registerProvider(scheme: string, provider: IFileSystemProvider): IDisposable {
@@ -258,9 +260,16 @@ export class RemoteFileService extends FileService {
// --- stat
private _withProvider(resource: URI): TPromise<IFileSystemProvider> {
if (!posix.isAbsolute(resource.path)) {
throw new FileOperationError(
localize('invalidPath', "The path of resource '{0}' must be absolute", resource.toString(true)),
FileOperationResult.FILE_INVALID_PATH
);
}
return TPromise.join([
this._extensionService.activateByEvent('onFileSystem:' + resource.scheme),
this._extensionService.activateByEvent('onFileSystemAccess:' + resource.scheme) // todo@remote -> remove
this._extensionService.activateByEvent('onFileSystem:' + resource.scheme)
]).then(() => {
const provider = this._provider.get(resource.scheme);
if (!provider) {
@@ -404,6 +413,7 @@ export class RemoteFileService extends FileService {
name: fileStat.name,
etag: fileStat.etag,
mtime: fileStat.mtime,
isReadonly: fileStat.isReadonly
};
});
});
@@ -434,12 +444,20 @@ export class RemoteFileService extends FileService {
}
}
private static _throwIfFileSystemIsReadonly(provider: IFileSystemProvider): IFileSystemProvider {
if (provider.capabilities & FileSystemProviderCapabilities.Readonly) {
throw new FileOperationError(localize('err.readonly', "Resource can not be modified."), FileOperationResult.FILE_PERMISSION_DENIED);
}
return provider;
}
createFile(resource: URI, content?: string, options?: ICreateFileOptions): TPromise<IFileStat> {
if (resource.scheme === Schemas.file) {
return super.createFile(resource, content, options);
} else {
return this._withProvider(resource).then(provider => {
return this._withProvider(resource).then(RemoteFileService._throwIfFileSystemIsReadonly).then(provider => {
return RemoteFileService._mkdirp(provider, resource.with({ path: posix.dirname(resource.path) })).then(() => {
const encoding = this.encoding.getWriteEncoding(resource);
return this._writeFile(provider, resource, new StringSnapshot(content), encoding, { create: true, overwrite: Boolean(options && options.overwrite) });
@@ -460,7 +478,7 @@ export class RemoteFileService extends FileService {
if (resource.scheme === Schemas.file) {
return super.updateContent(resource, value, options);
} else {
return this._withProvider(resource).then(provider => {
return this._withProvider(resource).then(RemoteFileService._throwIfFileSystemIsReadonly).then(provider => {
return RemoteFileService._mkdirp(provider, resource.with({ path: posix.dirname(resource.path) })).then(() => {
const snapshot = typeof value === 'string' ? new StringSnapshot(value) : value;
return this._writeFile(provider, resource, snapshot, options && options.encoding, { create: true, overwrite: true });
@@ -491,7 +509,8 @@ export class RemoteFileService extends FileService {
etag: content.etag,
mtime: content.mtime,
name: content.name,
resource: content.resource
resource: content.resource,
isReadonly: content.isReadonly
};
content.value.on('data', chunk => result.value += chunk);
content.value.on('error', reject);
@@ -501,12 +520,12 @@ export class RemoteFileService extends FileService {
// --- delete
del(resource: URI, useTrash?: boolean): TPromise<void> {
del(resource: URI, options?: { useTrash?: boolean, recursive?: boolean }): TPromise<void> {
if (resource.scheme === Schemas.file) {
return super.del(resource, useTrash);
return super.del(resource, options);
} else {
return this._withProvider(resource).then(provider => {
return provider.delete(resource).then(() => {
return this._withProvider(resource).then(RemoteFileService._throwIfFileSystemIsReadonly).then(provider => {
return provider.delete(resource, { recursive: options && options.recursive }).then(() => {
this._onAfterOperation.fire(new FileOperationEvent(resource, FileOperation.DELETE));
});
});
@@ -517,7 +536,7 @@ export class RemoteFileService extends FileService {
if (resource.scheme === Schemas.file) {
return super.createFolder(resource);
} else {
return this._withProvider(resource).then(provider => {
return this._withProvider(resource).then(RemoteFileService._throwIfFileSystemIsReadonly).then(provider => {
return RemoteFileService._mkdirp(provider, resource.with({ path: posix.dirname(resource.path) })).then(() => {
return provider.mkdir(resource).then(() => {
return this.resolveFile(resource);
@@ -530,15 +549,6 @@ export class RemoteFileService extends FileService {
}
}
rename(resource: URI, newName: string): TPromise<IFileStat, any> {
if (resource.scheme === Schemas.file) {
return super.rename(resource, newName);
} else {
const target = resource.with({ path: posix.join(resource.path, '..', newName) });
return this._doMoveWithInScheme(resource, target, false);
}
}
moveFile(source: URI, target: URI, overwrite?: boolean): TPromise<IFileStat> {
if (source.scheme !== target.scheme) {
return this._doMoveAcrossScheme(source, target);
@@ -552,10 +562,10 @@ export class RemoteFileService extends FileService {
private _doMoveWithInScheme(source: URI, target: URI, overwrite?: boolean): TPromise<IFileStat> {
const prepare = overwrite
? this.del(target).then(undefined, err => { /*ignore*/ })
? this.del(target, { recursive: true }).then(undefined, err => { /*ignore*/ })
: TPromise.as(null);
return prepare.then(() => this._withProvider(source)).then(provider => {
return prepare.then(() => this._withProvider(source)).then(RemoteFileService._throwIfFileSystemIsReadonly).then(provider => {
return provider.rename(source, target, { overwrite }).then(() => {
return this.resolveFile(target);
}).then(fileStat => {
@@ -573,7 +583,7 @@ export class RemoteFileService extends FileService {
private _doMoveAcrossScheme(source: URI, target: URI, overwrite?: boolean): TPromise<IFileStat> {
return this.copyFile(source, target, overwrite).then(() => {
return this.del(source);
return this.del(source, { recursive: true });
}).then(() => {
return this.resolveFile(target);
}).then(fileStat => {
@@ -587,7 +597,7 @@ export class RemoteFileService extends FileService {
return super.copyFile(source, target, overwrite);
}
return this._withProvider(target).then(provider => {
return this._withProvider(target).then(RemoteFileService._throwIfFileSystemIsReadonly).then(provider => {
if (source.scheme === target.scheme && (provider.capabilities & FileSystemProviderCapabilities.FileFolderCopy)) {
// good: provider supports copy withing scheme
@@ -606,7 +616,7 @@ export class RemoteFileService extends FileService {
}
const prepare = overwrite
? this.del(target).then(undefined, err => { /*ignore*/ })
? this.del(target, { recursive: true }).then(undefined, err => { /*ignore*/ })
: TPromise.as(null);
return prepare.then(() => {
@@ -641,7 +651,7 @@ export class RemoteFileService extends FileService {
private _activeWatches = new Map<string, { unwatch: Thenable<IDisposable>, count: number }>();
public watchFileChanges(resource: URI, opts?: IWatchOptions): void {
watchFileChanges(resource: URI, opts?: IWatchOptions): void {
if (resource.scheme === Schemas.file) {
return super.watchFileChanges(resource);
}
@@ -667,7 +677,7 @@ export class RemoteFileService extends FileService {
});
}
public unwatchFileChanges(resource: URI): void {
unwatchFileChanges(resource: URI): void {
if (resource.scheme === Schemas.file) {
return super.unwatchFileChanges(resource);
}

View File

@@ -9,11 +9,12 @@ import * as path from 'path';
import * as platform from 'vs/base/common/platform';
import * as watcher from 'vs/workbench/services/files/node/watcher/common';
import * as nsfw from 'vscode-nsfw';
import { IWatcherService, IWatcherRequest } from 'vs/workbench/services/files/node/watcher/nsfw/watcher';
import { TPromise, ProgressCallback, TValueCallback, ErrorCallback } from 'vs/base/common/winjs.base';
import { IWatcherService, IWatcherRequest, IWatcherOptions, IWatchError } from 'vs/workbench/services/files/node/watcher/nsfw/watcher';
import { TPromise, TValueCallback } from 'vs/base/common/winjs.base';
import { ThrottledDelayer } from 'vs/base/common/async';
import { FileChangeType } from 'vs/platform/files/common/files';
import { normalizeNFC } from 'vs/base/common/strings';
import { normalizeNFC } from 'vs/base/common/normalization';
import { Event, Emitter } from 'vs/base/common/event';
const nsfwActionToRawChangeType: { [key: number]: number } = [];
nsfwActionToRawChangeType[nsfw.actions.CREATED] = FileChangeType.ADDED;
@@ -35,20 +36,15 @@ 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 _watcherPromise: TPromise<void>;
private _progressCallback: ProgressCallback;
private _errorCallback: ErrorCallback;
private _verboseLogging: boolean;
private enospcErrorLogged: boolean;
public initialize(verboseLogging: boolean): TPromise<void> {
this._verboseLogging = true;
this._watcherPromise = new TPromise<void>((c, e, p) => {
this._errorCallback = e;
this._progressCallback = p;
private _onWatchEvent = new Emitter<watcher.IRawFileChange[] | IWatchError>();
readonly onWatchEvent = this._onWatchEvent.event;
});
return this._watcherPromise;
watch(options: IWatcherOptions): Event<watcher.IRawFileChange[] | IWatchError> {
this._verboseLogging = options.verboseLogging;
return this.onWatchEvent;
}
private _watch(request: IWatcherRequest): void {
@@ -70,7 +66,7 @@ export class NsfwWatcherService implements IWatcherService {
// See https://github.com/Microsoft/vscode/issues/7950
if (e === 'Inotify limit reached' && !this.enospcErrorLogged) {
this.enospcErrorLogged = true;
this._errorCallback(new Error('Inotify limit reached (ENOSPC)'));
this._onWatchEvent.fire({ message: 'Inotify limit reached (ENOSPC)' });
}
});
@@ -119,7 +115,7 @@ export class NsfwWatcherService implements IWatcherService {
// Broadcast to clients normalized
const res = watcher.normalize(events);
this._progressCallback(res);
this._onWatchEvent.fire(res);
// Logging
if (this._verboseLogging) {

View File

@@ -6,13 +6,23 @@
'use strict';
import { TPromise } from 'vs/base/common/winjs.base';
import { Event } from 'vs/base/common/event';
import { IRawFileChange } from 'vs/workbench/services/files/node/watcher/common';
export interface IWatcherRequest {
basePath: string;
ignored: string[];
}
export interface IWatcherOptions {
verboseLogging: boolean;
}
export interface IWatchError {
message: string;
}
export interface IWatcherService {
initialize(verboseLogging: boolean): TPromise<void>;
watch(options: IWatcherOptions): Event<IRawFileChange[] | IWatchError>;
setRoots(roots: IWatcherRequest[]): TPromise<void>;
}

View File

@@ -7,21 +7,31 @@
import { TPromise } from 'vs/base/common/winjs.base';
import { IChannel } from 'vs/base/parts/ipc/common/ipc';
import { IWatcherRequest, IWatcherService } from './watcher';
import { IWatcherRequest, IWatcherService, IWatcherOptions, IWatchError } from './watcher';
import { Event } from 'vs/base/common/event';
import { IRawFileChange } from 'vs/workbench/services/files/node/watcher/common';
export interface IWatcherChannel extends IChannel {
call(command: 'initialize', verboseLogging: boolean): TPromise<void>;
listen(event: 'watch', verboseLogging: boolean): Event<IRawFileChange[] | Error>;
listen<T>(event: string, arg?: any): Event<T>;
call(command: 'setRoots', request: IWatcherRequest[]): TPromise<void>;
call(command: string, arg: any): TPromise<any>;
call<T>(command: string, arg?: any): TPromise<T>;
}
export class WatcherChannel implements IWatcherChannel {
constructor(private service: IWatcherService) { }
listen(event: string, arg?: any): Event<any> {
switch (event) {
case 'watch': return this.service.watch(arg);
}
throw new Error('No events');
}
call(command: string, arg: any): TPromise<any> {
switch (command) {
case 'initialize': return this.service.initialize(arg);
case 'setRoots': return this.service.setRoots(arg);
}
return undefined;
@@ -32,8 +42,8 @@ export class WatcherChannelClient implements IWatcherService {
constructor(private channel: IWatcherChannel) { }
initialize(verboseLogging: boolean): TPromise<void> {
return this.channel.call('initialize', verboseLogging);
watch(options: IWatcherOptions): Event<IRawFileChange[] | IWatchError> {
return this.channel.listen('watch', options);
}
setRoots(roots: IWatcherRequest[]): TPromise<void> {

View File

@@ -5,7 +5,6 @@
'use strict';
import { TPromise } from 'vs/base/common/winjs.base';
import { getNextTickChannel } from 'vs/base/parts/ipc/common/ipc';
import { Client } from 'vs/base/parts/ipc/node/ipc.cp';
import uri from 'vs/base/common/uri';
@@ -16,7 +15,8 @@ import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { Schemas } from 'vs/base/common/network';
import { isPromiseCanceledError } from 'vs/base/common/errors';
import { filterEvent } from 'vs/base/common/event';
import { IWatchError } from 'vs/workbench/services/files/node/watcher/nsfw/watcher';
export class FileWatcher {
private static readonly MAX_RESTARTS = 5;
@@ -24,7 +24,7 @@ export class FileWatcher {
private service: WatcherChannelClient;
private isDisposed: boolean;
private restartCounter: number;
private toDispose: IDisposable[];
private toDispose: IDisposable[] = [];
constructor(
private contextService: IWorkspaceContextService,
@@ -35,17 +35,14 @@ export class FileWatcher {
) {
this.isDisposed = false;
this.restartCounter = 0;
this.toDispose = [];
}
public startWatching(): () => void {
const args = ['--type=watcherService'];
const client = new Client(
uri.parse(require.toUrl('bootstrap')).fsPath,
{
serverName: 'Watcher',
args,
args: ['--type=watcherService'],
env: {
AMD_ENTRYPOINT: 'vs/workbench/services/files/node/watcher/nsfw/watcherApp',
PIPE_LOGGING: 'true',
@@ -55,16 +52,7 @@ export class FileWatcher {
);
this.toDispose.push(client);
// Initialize watcher
const channel = getNextTickChannel(client.getChannel<IWatcherChannel>('watcher'));
this.service = new WatcherChannelClient(channel);
this.service.initialize(this.verboseLogging).then(null, err => {
if (!this.isDisposed && !isPromiseCanceledError(err)) {
return TPromise.wrapError(err); // the service lib uses the promise cancel error to indicate the process died, we do not want to bubble this up
}
return void 0;
}, (events: IRawFileChange[]) => this.onRawFileEvents(events)).done(() => {
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) {
@@ -76,11 +64,20 @@ export class FileWatcher {
this.errorLogger('[FileWatcher] failed to start after retrying for some time, giving up. Please report this as a bug report!');
}
}
}, error => {
if (!this.isDisposed) {
this.errorLogger(error);
}
});
}, null, this.toDispose);
// Initialize watcher
const channel = getNextTickChannel(client.getChannel<IWatcherChannel>('watcher'));
this.service = new WatcherChannelClient(channel);
const options = { verboseLogging: this.verboseLogging };
const onWatchEvent = filterEvent(this.service.watch(options), () => !this.isDisposed);
const onError = filterEvent<any, IWatchError>(onWatchEvent, (e): e is IWatchError => typeof e.message === 'string');
onError(err => this.errorLogger(err.message), null, this.toDispose);
const onFileChanges = filterEvent<any, IRawFileChange[]>(onWatchEvent, (e): e is IRawFileChange[] => Array.isArray(e) && e.length > 0);
onFileChanges(e => this.onFileChanges(toFileChangesEvent(e)), null, this.toDispose);
// Start watching
this.updateFolders();
@@ -118,17 +115,6 @@ export class FileWatcher {
}));
}
private onRawFileEvents(events: IRawFileChange[]): void {
if (this.isDisposed) {
return;
}
// Emit through event emitter
if (events.length > 0) {
this.onFileChanges(toFileChangesEvent(events));
}
}
private dispose(): void {
this.isDisposed = true;
this.toDispose = dispose(this.toDispose);

View File

@@ -10,165 +10,334 @@ import * as fs from 'fs';
import * as gracefulFs from 'graceful-fs';
gracefulFs.gracefulify(fs);
import * as paths from 'vs/base/common/paths';
import * as glob from 'vs/base/common/glob';
import { TPromise } from 'vs/base/common/winjs.base';
import { FileChangeType } from 'vs/platform/files/common/files';
import { ThrottledDelayer } from 'vs/base/common/async';
import * as strings from 'vs/base/common/strings';
import { normalizeNFC } from 'vs/base/common/normalization';
import { realcaseSync } from 'vs/base/node/extfs';
import { isMacintosh } from 'vs/base/common/platform';
import * as watcher from 'vs/workbench/services/files/node/watcher/common';
import { IWatcherRequest, IWatcherService } from 'vs/workbench/services/files/node/watcher/unix/watcher';
import { IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle';
import * as watcherCommon from 'vs/workbench/services/files/node/watcher/common';
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 _options: IWatcherOptions & IChockidarWatcherOptions;
private spamCheckStartTime: number;
private spamWarningLogged: boolean;
private enospcErrorLogged: boolean;
private toDispose: IDisposable[] = [];
public watch(request: IWatcherRequest): TPromise<void> {
private _onWatchEvent = new Emitter<watcherCommon.IRawFileChange[] | IWatchError>();
readonly onWatchEvent = this._onWatchEvent.event;
watch(options: IWatcherOptions & IChockidarWatcherOptions): Event<watcherCommon.IRawFileChange[] | IWatchError> {
this._options = options;
this._watchers = Object.create(null);
this._watcherCount = 0;
return this.onWatchEvent;
}
public setRoots(requests: IWatcherRequest[]): TPromise<void> {
const watchers = Object.create(null);
const newRequests = [];
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 TPromise.as(null);
}
// for test purposes
public get wacherCount() {
return this._watcherCount;
}
private _watch(basePath: string, requests: IWatcherRequest[]): IWatcher {
if (this._options.verboseLogging) {
console.log(`Start watching: ${basePath}]`);
}
const pollingInterval = this._options.pollingInterval || 1000;
const watcherOpts: chokidar.IOptions = {
ignoreInitial: true,
ignorePermissionErrors: true,
followSymlinks: true, // this is the default of chokidar and supports file events through symlinks
ignored: request.ignored,
interval: 1000, // while not used in normal cases, if any error causes chokidar to fallback to polling, increase its intervals
binaryInterval: 1000,
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
};
// if there's only one request, use the built-in ignore-filterering
if (requests.length === 1) {
watcherOpts.ignored = requests[0].ignored;
}
// 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 originalBasePath = request.basePath;
const realBasePath = isMacintosh ? (realcaseSync(originalBasePath) || originalBasePath) : originalBasePath;
const realBasePath = isMacintosh ? (realcaseSync(basePath) || basePath) : basePath;
const realBasePathLength = realBasePath.length;
const realBasePathDiffers = (originalBasePath !== realBasePath);
const realBasePathDiffers = (basePath !== realBasePath);
if (realBasePathDiffers) {
console.warn(`Watcher basePath does not match version on disk and was corrected (original: ${originalBasePath}, real: ${realBasePath})`);
console.warn(`Watcher basePath does not match version on disk and was corrected (original: ${basePath}, real: ${realBasePath})`);
}
const chokidarWatcher = chokidar.watch(realBasePath, watcherOpts);
let chokidarWatcher = 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: watcher.IRawFileChange[] = [];
const fileEventDelayer = new ThrottledDelayer(ChokidarWatcherService.FS_EVENT_DELAY);
let undeliveredFileEvents: watcherCommon.IRawFileChange[] = [];
let fileEventDelayer = new ThrottledDelayer(ChokidarWatcherService.FS_EVENT_DELAY);
this.toDispose.push(toDisposable(() => {
chokidarWatcher.close();
fileEventDelayer.cancel();
}));
return new TPromise<void>((c, e, p) => {
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 = strings.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 = originalBasePath + path.substr(realBasePathLength);
}
let event: watcher.IRawFileChange = null;
// Change
if (type === 'change') {
event = { type: 0, path };
}
// Add
else if (type === 'add' || type === 'addDir') {
event = { type: 1, path };
}
// Delete
else if (type === 'unlink' || type === 'unlinkDir') {
event = { type: 2, path };
}
if (event) {
// Logging
if (request.verboseLogging) {
console.log(event.type === FileChangeType.ADDED ? '[ADDED]' : event.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]', event.path);
const watcher: IWatcher = {
requests,
stop: () => {
try {
if (this._options.verboseLogging) {
console.log(`Stop watching: ${basePath}]`);
}
// 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(strings.format('Watcher is busy catching up with {0} file changes in 60 seconds. Latest changed path is "{1}"', undeliveredFileEvents.length, event.path));
if (chokidarWatcher) {
chokidarWatcher.close();
this._watcherCount--;
chokidarWatcher = null;
}
if (fileEventDelayer) {
fileEventDelayer.cancel();
fileEventDelayer = null;
}
} catch (error) {
console.error(error.toString());
}
}
};
// Add to buffer
undeliveredFileEvents.push(event);
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);
}
// Delay and send buffer
fileEventDelayer.trigger(() => {
const events = undeliveredFileEvents;
undeliveredFileEvents = [];
if (path.indexOf(realBasePath) < 0) {
return; // we really only care about absolute paths here in our basepath context here
}
// Broadcast to clients normalized
const res = watcher.normalize(events);
p(res);
// Make sure to convert the path back to its original basePath form if the realpath is different
if (realBasePathDiffers) {
path = basePath + path.substr(realBasePathLength);
}
// Logging
if (request.verboseLogging) {
res.forEach(r => {
console.log(' >> normalized', r.type === FileChangeType.ADDED ? '[ADDED]' : r.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]', r.path);
});
}
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;
}
return TPromise.as(null);
if (isIgnored(path, watcher.requests)) {
return;
}
let event = { type: eventType, path };
// Logging
if (this._options.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(strings.format('Watcher is busy catching up with {0} file changes in 60 seconds. Latest changed path is "{1}"', undeliveredFileEvents.length, event.path));
}
// Add to buffer
undeliveredFileEvents.push(event);
// Delay and send buffer
fileEventDelayer.trigger(() => {
const events = undeliveredFileEvents;
undeliveredFileEvents = [];
// Broadcast to clients normalized
const res = watcherCommon.normalize(events);
this._onWatchEvent.fire(res);
// Logging
if (this._options.verboseLogging) {
res.forEach(r => {
console.log(' >> normalized', r.type === FileChangeType.ADDED ? '[ADDED]' : r.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]', r.path);
});
}
});
chokidarWatcher.on('error', (error: Error) => {
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 ((<any>error).code === 'ENOSPC') {
if (!this.enospcErrorLogged) {
this.enospcErrorLogged = true;
e(new Error('Inotify limit reached (ENOSPC)'));
}
} else {
console.error(error.toString());
}
}
return TPromise.as(null);
});
}, () => {
this.toDispose = dispose(this.toDispose);
});
chokidarWatcher.on('error', (error: Error) => {
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 ((<any>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(): TPromise<void> {
this.toDispose = dispose(this.toDispose);
for (let path in this._watchers) {
let watcher = this._watchers[path];
watcher.stop();
}
this._watchers = Object.create(null);
return TPromise.as(void 0);
}
}
function isIgnored(path: string, requests: ExtendedWatcherRequest[]): boolean {
for (let request of requests) {
if (request.basePath === path) {
return false;
}
if (paths.isEqualOrParent(path, request.basePath)) {
if (!request.parsedPattern) {
if (request.ignored && request.ignored.length > 0) {
let pattern = `{${request.ignored.map(i => i + '/**').join(',')}}`;
request.parsedPattern = glob.parse(pattern);
} else {
request.parsedPattern = () => false;
}
}
const relPath = path.substr(request.basePath.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.basePath.localeCompare(r2.basePath));
let prevRequest: IWatcherRequest = null;
let result: { [basePath: string]: IWatcherRequest[] } = Object.create(null);
for (let request of requests) {
let basePath = request.basePath;
let ignored = (request.ignored || []).sort();
if (prevRequest && (paths.isEqualOrParent(basePath, prevRequest.basePath))) {
if (!isEqualIgnore(ignored, prevRequest.ignored)) {
result[prevRequest.basePath].push({ basePath, ignored });
}
} else {
prevRequest = { basePath, 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].basePath !== r2[k].basePath || !isEqualIgnore(r1[k].ignored, r2[k].ignored)) {
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;
}

View File

@@ -0,0 +1,323 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as assert from 'assert';
import * as os from 'os';
import * as path from '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 { IRawFileChange } from 'vs/workbench/services/files/node/watcher/common';
import { FileChangeType } from 'vs/platform/files/common/files';
function newRequest(basePath: string, ignored = []): IWatcherRequest {
return { basePath, 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.basePath.localeCompare(r2.basePath));
let e = expectedRequests[path].sort((r1, r2) => r1.basePath.localeCompare(r2.basePath));
assert.deepEqual(a, e);
}
}
function sort(changes: IRawFileChange[]) {
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: IRawFileChange[], expected: IRawFileChange[]) {
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: IRawFileChange[] = [];
let error = 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.del(testDir);
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 = { basePath: testDir, ignored: [] };
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.del(copiedFilePath);
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.del(testFolderPath);
await assertFileEvents(result, [{ path: testFolderPath, type: FileChangeType.DELETED }, { path: renamedFilePath, type: FileChangeType.DELETED }]);
});
test('simple file operations, ignore', async () => {
let request: IWatcherRequest = { basePath: testDir, ignored: ['**/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.del(movedFile1); // hidden
await pfs.del(movedFile2);
await pfs.del(movedFile3);
await pfs.del(folder1); // hidden
await pfs.del(folder2); // hidden
await pfs.del(folder3); // hidden
await pfs.del(folder4);
await pfs.del(folder5);
await pfs.del(file4);
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 = { basePath: aFolder, ignored: ['**/*.js'] };
let request2: IWatcherRequest = { basePath: b2Folder, ignored: ['**/*.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 = { basePath: aFolder, ignored: ['**/*.json'] };
service.setRoots([request3]);
await wait(300);
assert.equal(service.wacherCount, 1);
// delete all
await pfs.del(folderPath1);
await pfs.del(folderPath2);
await pfs.del(filePath4);
await assertFileEvents(result, [{ path: folderPath1, type: FileChangeType.DELETED }, { path: filePath2, type: FileChangeType.DELETED }]);
});
test('simple file operations, nested roots', async () => {
let request1: IWatcherRequest = { basePath: testDir, ignored: ['**/b2'] };
let request2: IWatcherRequest = { basePath: bFolder, ignored: ['**/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.del(folderPath1);
await pfs.del(filePath1);
await assertFileEvents(result, [{ path: filePath1, type: FileChangeType.DELETED }]);
});
});

View File

@@ -6,13 +6,23 @@
'use strict';
import { TPromise } from 'vs/base/common/winjs.base';
import { Event } from 'vs/base/common/event';
import { IRawFileChange } from 'vs/workbench/services/files/node/watcher/common';
export interface IWatcherRequest {
basePath: string;
ignored: string[];
}
export interface IWatcherOptions {
verboseLogging: boolean;
}
export interface IWatcherService {
watch(request: IWatcherRequest): TPromise<void>;
export interface IWatchError {
message: string;
}
export interface IWatcherService {
watch(options: IWatcherOptions): Event<IRawFileChange[] | IWatchError>;
setRoots(roots: IWatcherRequest[]): TPromise<void>;
}

View File

@@ -7,20 +7,32 @@
import { TPromise } from 'vs/base/common/winjs.base';
import { IChannel } from 'vs/base/parts/ipc/common/ipc';
import { IWatcherRequest, IWatcherService } from 'vs/workbench/services/files/node/watcher/unix/watcher';
import { IWatcherRequest, IWatcherService, IWatcherOptions, IWatchError } from './watcher';
import { Event } from 'vs/base/common/event';
import { IRawFileChange } from 'vs/workbench/services/files/node/watcher/common';
export interface IWatcherChannel extends IChannel {
call(command: 'watch', request: IWatcherRequest): TPromise<void>;
call(command: string, arg: any): TPromise<any>;
listen(event: 'watch', verboseLogging: boolean): Event<IRawFileChange[] | Error>;
listen<T>(event: string, arg?: any): Event<T>;
call(command: 'setRoots', request: IWatcherRequest[]): TPromise<void>;
call<T>(command: string, arg?: any): TPromise<T>;
}
export class WatcherChannel implements IWatcherChannel {
constructor(private service: IWatcherService) { }
listen(event: string, arg?: any): Event<any> {
switch (event) {
case 'watch': return this.service.watch(arg);
}
throw new Error('No events');
}
call(command: string, arg: any): TPromise<any> {
switch (command) {
case 'watch': return this.service.watch(arg);
case 'setRoots': return this.service.setRoots(arg);
}
return undefined;
}
@@ -30,7 +42,11 @@ export class WatcherChannelClient implements IWatcherService {
constructor(private channel: IWatcherChannel) { }
watch(request: IWatcherRequest): TPromise<void> {
return this.channel.call('watch', request);
watch(options: IWatcherOptions): Event<IRawFileChange[] | IWatchError> {
return this.channel.listen('watch', options);
}
setRoots(roots: IWatcherRequest[]): TPromise<void> {
return this.channel.call('setRoots', roots);
}
}

View File

@@ -5,32 +5,37 @@
'use strict';
import { TPromise } from 'vs/base/common/winjs.base';
import { getNextTickChannel } from 'vs/base/parts/ipc/common/ipc';
import { Client } from 'vs/base/parts/ipc/node/ipc.cp';
import uri from 'vs/base/common/uri';
import { toFileChangesEvent, IRawFileChange } from 'vs/workbench/services/files/node/watcher/common';
import { IWatcherChannel, WatcherChannelClient } from 'vs/workbench/services/files/node/watcher/unix/watcherIpc';
import { FileChangesEvent } from 'vs/platform/files/common/files';
import { FileChangesEvent, IFilesConfiguration } from 'vs/platform/files/common/files';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { normalize } from 'path';
import { isPromiseCanceledError } from 'vs/base/common/errors';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { Schemas } from 'vs/base/common/network';
import { filterEvent } from 'vs/base/common/event';
import { IWatchError } from 'vs/workbench/services/files/node/watcher/unix/watcher';
export class FileWatcher {
private static readonly MAX_RESTARTS = 5;
private isDisposed: boolean;
private restartCounter: number;
private service: WatcherChannelClient;
private toDispose: IDisposable[];
constructor(
private contextService: IWorkspaceContextService,
private ignored: string[],
private configurationService: IConfigurationService,
private onFileChanges: (changes: FileChangesEvent) => void,
private errorLogger: (msg: string) => void,
private verboseLogging: boolean
) {
this.isDisposed = false;
this.restartCounter = 0;
this.toDispose = [];
}
public startWatching(): () => void {
@@ -48,20 +53,9 @@ export class FileWatcher {
}
}
);
this.toDispose.push(client);
const channel = getNextTickChannel(client.getChannel<IWatcherChannel>('watcher'));
const service = new WatcherChannelClient(channel);
// Start watching
const basePath: string = normalize(this.contextService.getWorkspace().folders[0].uri.fsPath);
service.watch({ basePath: basePath, ignored: this.ignored, verboseLogging: this.verboseLogging }).then(null, err => {
if (!this.isDisposed && !isPromiseCanceledError(err)) {
return TPromise.wrapError(err); // the service lib uses the promise cancel error to indicate the process died, we do not want to bubble this up
}
return void 0;
}, (events: IRawFileChange[]) => this.onRawFileEvents(events)).done(() => {
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) {
@@ -73,26 +67,59 @@ export class FileWatcher {
this.errorLogger('[FileWatcher] failed to start after retrying for some time, giving up. Please report this as a bug report!');
}
}
}, error => {
if (!this.isDisposed) {
this.errorLogger(error);
}
});
}, null, this.toDispose);
return () => {
this.isDisposed = true;
client.dispose();
};
const channel = getNextTickChannel(client.getChannel<IWatcherChannel>('watcher'));
this.service = new WatcherChannelClient(channel);
const options = { verboseLogging: this.verboseLogging };
const onWatchEvent = filterEvent(this.service.watch(options), () => !this.isDisposed);
const onError = filterEvent<any, IWatchError>(onWatchEvent, (e): e is IWatchError => typeof e.message === 'string');
onError(err => this.errorLogger(err.message), null, this.toDispose);
const onFileChanges = filterEvent<any, IRawFileChange[]>(onWatchEvent, (e): e is IRawFileChange[] => Array.isArray(e) && e.length > 0);
onFileChanges(e => this.onFileChanges(toFileChangesEvent(e)), null, this.toDispose);
// Start watching
this.updateFolders();
this.toDispose.push(this.contextService.onDidChangeWorkspaceFolders(() => this.updateFolders()));
this.toDispose.push(this.configurationService.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('files.watcherExclude')) {
this.updateFolders();
}
}));
return () => this.dispose();
}
private onRawFileEvents(events: IRawFileChange[]): void {
private updateFolders() {
if (this.isDisposed) {
return;
}
// Emit through event emitter
if (events.length > 0) {
this.onFileChanges(toFileChangesEvent(events));
}
this.service.setRoots(this.contextService.getWorkspace().folders.filter(folder => {
// Only workspace folders on disk
return folder.uri.scheme === Schemas.file;
}).map(folder => {
// Fetch the root's watcherExclude setting and return it
const configuration = this.configurationService.getValue<IFilesConfiguration>({
resource: folder.uri
});
let ignored: string[] = [];
if (configuration.files && configuration.files.watcherExclude) {
ignored = Object.keys(configuration.files.watcherExclude).filter(k => !!configuration.files.watcherExclude[k]);
}
return {
basePath: folder.uri.fsPath,
ignored,
recursive: false
};
}));
}
private dispose(): void {
this.isDisposed = true;
this.toDispose = dispose(this.toDispose);
}
}

View File

@@ -12,6 +12,7 @@ import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace
import { normalize } from 'path';
import { rtrim, endsWith } from 'vs/base/common/strings';
import { sep } from 'vs/base/common/paths';
import { Schemas } from 'vs/base/common/network';
export class FileWatcher {
private isDisposed: boolean;
@@ -26,6 +27,9 @@ export class FileWatcher {
}
public startWatching(): () => void {
if (this.contextService.getWorkspace().folders[0].uri.scheme !== Schemas.file) {
return () => { };
}
let basePath: string = normalize(this.contextService.getWorkspace().folders[0].uri.fsPath);
if (basePath && basePath.indexOf('\\\\') === 0 && endsWith(basePath, sep)) {

View File

@@ -157,7 +157,7 @@ suite('FileService', () => {
const resource = uri.file(path.join(testDir, 'index.html'));
return service.resolveFile(resource).then(source => {
return service.rename(source.resource, 'other.html').then(renamed => {
return service.moveFile(source.resource, uri.file(path.join(path.dirname(source.resource.fsPath), 'other.html'))).then(renamed => {
assert.equal(fs.existsSync(renamed.resource.fsPath), true);
assert.equal(fs.existsSync(source.resource.fsPath), false);
@@ -181,7 +181,7 @@ suite('FileService', () => {
const resource = uri.file(path.join(testDir, 'index.html'));
return service.resolveFile(resource).then(source => {
return service.rename(source.resource, renameToPath).then(renamed => {
return service.moveFile(source.resource, uri.file(path.join(path.dirname(source.resource.fsPath), renameToPath))).then(renamed => {
assert.equal(fs.existsSync(renamed.resource.fsPath), true);
assert.equal(fs.existsSync(source.resource.fsPath), false);
@@ -202,7 +202,7 @@ suite('FileService', () => {
const resource = uri.file(path.join(testDir, 'deep'));
return service.resolveFile(resource).then(source => {
return service.rename(source.resource, 'deeper').then(renamed => {
return service.moveFile(source.resource, uri.file(path.join(path.dirname(source.resource.fsPath), 'deeper'))).then(renamed => {
assert.equal(fs.existsSync(renamed.resource.fsPath), true);
assert.equal(fs.existsSync(source.resource.fsPath), false);
@@ -226,7 +226,7 @@ suite('FileService', () => {
const resource = uri.file(path.join(testDir, 'deep'));
return service.resolveFile(resource).then(source => {
return service.rename(source.resource, renameToPath).then(renamed => {
return service.moveFile(source.resource, uri.file(path.join(path.dirname(source.resource.fsPath), renameToPath))).then(renamed => {
assert.equal(fs.existsSync(renamed.resource.fsPath), true);
assert.equal(fs.existsSync(source.resource.fsPath), false);
@@ -246,7 +246,7 @@ suite('FileService', () => {
const resource = uri.file(path.join(testDir, 'index.html'));
return service.resolveFile(resource).then(source => {
return service.rename(source.resource, 'INDEX.html').then(renamed => {
return service.moveFile(source.resource, uri.file(path.join(path.dirname(source.resource.fsPath), 'INDEX.html'))).then(renamed => {
assert.equal(fs.existsSync(renamed.resource.fsPath), true);
assert.equal(path.basename(renamed.resource.fsPath), 'INDEX.html');
@@ -430,7 +430,7 @@ suite('FileService', () => {
test('copyFile - MIX CASE', function () {
return service.resolveFile(uri.file(path.join(testDir, 'index.html'))).then(source => {
return service.rename(source.resource, 'CONWAY.js').then(renamed => { // index.html => CONWAY.js
return service.moveFile(source.resource, uri.file(path.join(path.dirname(source.resource.fsPath), 'CONWAY.js'))).then(renamed => {
assert.equal(fs.existsSync(renamed.resource.fsPath), true);
assert.ok(fs.readdirSync(testDir).some(f => f === 'CONWAY.js'));
@@ -476,7 +476,7 @@ suite('FileService', () => {
});
});
test('deleteFolder', function () {
test('deleteFolder (recursive)', function () {
let event: FileOperationEvent;
const toDispose = service.onAfterOperation(e => {
event = e;
@@ -484,7 +484,7 @@ suite('FileService', () => {
const resource = uri.file(path.join(testDir, 'deep'));
return service.resolveFile(resource).then(source => {
return service.del(source.resource).then(() => {
return service.del(source.resource, { recursive: true }).then(() => {
assert.equal(fs.existsSync(source.resource.fsPath), false);
assert.ok(event);
@@ -495,6 +495,17 @@ suite('FileService', () => {
});
});
test('deleteFolder (non recursive)', function () {
const resource = uri.file(path.join(testDir, 'deep'));
return service.resolveFile(resource).then(source => {
return service.del(source.resource).then(() => {
return TPromise.wrapError(new Error('Unexpected'));
}, error => {
return TPromise.as(true);
});
});
});
test('resolveFile', function () {
return service.resolveFile(uri.file(testDir), { resolveTo: [uri.file(path.join(testDir, 'deep'))] }).then(r => {
assert.equal(r.children.length, 8);

View File

@@ -0,0 +1,479 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { Event } from 'vs/base/common/event';
import { createDecorator, ServiceIdentifier, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { IEditorInput, IEditor, GroupIdentifier, IEditorInputWithOptions, CloseDirection } from 'vs/workbench/common/editor';
import { IEditorOptions, ITextEditorOptions } from 'vs/platform/editor/common/editor';
import { TPromise } from 'vs/base/common/winjs.base';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
export const IEditorGroupsService = createDecorator<IEditorGroupsService>('editorGroupsService');
export enum GroupDirection {
UP,
DOWN,
LEFT,
RIGHT
}
export function preferredSideBySideGroupDirection(configurationService: IConfigurationService): GroupDirection.DOWN | GroupDirection.RIGHT {
const openSideBySideDirection = configurationService.getValue<'right' | 'down'>('workbench.editor.openSideBySideDirection');
if (openSideBySideDirection === 'down') {
return GroupDirection.DOWN;
}
return GroupDirection.RIGHT;
}
export enum GroupOrientation {
HORIZONTAL,
VERTICAL
}
export enum GroupLocation {
FIRST,
LAST,
NEXT,
PREVIOUS
}
export interface IFindGroupScope {
direction?: GroupDirection;
location?: GroupLocation;
}
export enum GroupsArrangement {
/**
* Make the current active group consume the maximum
* amount of space possible.
*/
MINIMIZE_OTHERS,
/**
* Size all groups evenly.
*/
EVEN
}
export interface GroupLayoutArgument {
size?: number;
groups?: GroupLayoutArgument[];
}
export interface EditorGroupLayout {
orientation: GroupOrientation;
groups: GroupLayoutArgument[];
}
export interface IMoveEditorOptions {
index?: number;
inactive?: boolean;
preserveFocus?: boolean;
}
export interface ICopyEditorOptions extends IMoveEditorOptions { }
export interface IAddGroupOptions {
activate?: boolean;
}
export enum MergeGroupMode {
COPY_EDITORS,
MOVE_EDITORS
}
export interface IMergeGroupOptions {
mode?: MergeGroupMode;
index?: number;
}
export type ICloseEditorsFilter = {
except?: IEditorInput,
direction?: CloseDirection,
savedOnly?: boolean
};
export interface IEditorReplacement {
editor: IEditorInput;
replacement: IEditorInput;
options?: IEditorOptions | ITextEditorOptions;
}
export enum GroupsOrder {
/**
* Groups sorted by creation order (oldest one first)
*/
CREATION_TIME,
/**
* Groups sorted by most recent activity (most recent active first)
*/
MOST_RECENTLY_ACTIVE,
/**
* Groups sorted by grid widget order
*/
GRID_APPEARANCE
}
export enum EditorsOrder {
/**
* Editors sorted by most recent activity (most recent active first)
*/
MOST_RECENTLY_ACTIVE,
/**
* Editors sorted by sequential order
*/
SEQUENTIAL
}
export interface IEditorGroupsService {
_serviceBrand: ServiceIdentifier<any>;
/**
* An event for when the active editor group changes. The active editor
* group is the default location for new editors to open.
*/
readonly onDidActiveGroupChange: Event<IEditorGroup>;
/**
* An event for when a new group was added.
*/
readonly onDidAddGroup: Event<IEditorGroup>;
/**
* An event for when a group was removed.
*/
readonly onDidRemoveGroup: Event<IEditorGroup>;
/**
* An event for when a group was moved.
*/
readonly onDidMoveGroup: Event<IEditorGroup>;
/**
* An active group is the default location for new editors to open.
*/
readonly activeGroup: IEditorGroup;
/**
* All groups that are currently visible in the editor area in the
* order of their creation (oldest first).
*/
readonly groups: ReadonlyArray<IEditorGroup>;
/**
* The number of editor groups that are currently opened.
*/
readonly count: number;
/**
* The current layout orientation of the root group.
*/
readonly orientation: GroupOrientation;
/**
* Get all groups that are currently visible in the editor area optionally
* sorted by being most recent active or grid order. Will sort by creation
* time by default (oldest group first).
*/
getGroups(order?: GroupsOrder): ReadonlyArray<IEditorGroup>;
/**
* Allows to convert a group identifier to a group.
*/
getGroup(identifier: GroupIdentifier): IEditorGroup;
/**
* Set a group as active. An active group is the default location for new editors to open.
*/
activateGroup(group: IEditorGroup | GroupIdentifier): IEditorGroup;
/**
* Returns the size of a group.
*/
getSize(group: IEditorGroup | GroupIdentifier): number;
/**
* Sets the size of a group.
*/
setSize(group: IEditorGroup | GroupIdentifier, size: number): void;
/**
* Arrange all groups according to the provided arrangement.
*/
arrangeGroups(arrangement: GroupsArrangement): void;
/**
* Applies the provided layout by either moving existing groups or creating new groups.
*/
applyLayout(layout: EditorGroupLayout): void;
/**
* Sets the orientation of the root group to be either vertical or horizontal.
*/
setGroupOrientation(orientation: GroupOrientation): void;
/**
* Find a groupd in a specific scope:
* * `GroupLocation.FIRST`: the first group
* * `GroupLocation.LAST`: the last group
* * `GroupLocation.NEXT`: the next group from either the active one or `source`
* * `GroupLocation.PREVIOUS`: the previous group from either the active one or `source`
* * `GroupDirection.UP`: the next group above the active one or `source`
* * `GroupDirection.DOWN`: the next group below the active one or `source`
* * `GroupDirection.LEFT`: the next group to the left of the active one or `source`
* * `GroupDirection.RIGHT`: the next group to the right of the active one or `source`
*
* @param scope the scope of the group to search in
* @param source optional source to search from
* @param wrap optionally wrap around if reaching the edge of groups
*/
findGroup(scope: IFindGroupScope, source?: IEditorGroup | GroupIdentifier, wrap?: boolean): IEditorGroup;
/**
* Add a new group to the editor area. A new group is added by splitting a provided one in
* one of the four directions.
*
* @param location the group from which to split to add a new group
* @param direction the direction of where to split to
* @param options configure the newly group with options
*/
addGroup(location: IEditorGroup | GroupIdentifier, direction: GroupDirection, options?: IAddGroupOptions): IEditorGroup;
/**
* Remove a group from the editor area.
*/
removeGroup(group: IEditorGroup | GroupIdentifier): void;
/**
* Move a group to a new group in the editor area.
*
* @param group the group to move
* @param location the group from which to split to add the moved group
* @param direction the direction of where to split to
*/
moveGroup(group: IEditorGroup | GroupIdentifier, location: IEditorGroup | GroupIdentifier, direction: GroupDirection): IEditorGroup;
/**
* Merge the editors of a group into a target group. By default, all editors will
* move and the source group will close. This behaviour can be configured via the
* `IMergeGroupOptions` options.
*
* @param group the group to merge
* @param target the target group to merge into
* @param options controls how the merge should be performed. by default all editors
* will be moved over to the target and the source group will close. Configure to
* `MOVE_EDITORS_KEEP_GROUP` to prevent the source group from closing. Set to
* `COPY_EDITORS` to copy the editors into the target instead of moding them.
*/
mergeGroup(group: IEditorGroup | GroupIdentifier, target: IEditorGroup | GroupIdentifier, options?: IMergeGroupOptions): IEditorGroup;
/**
* Copy a group to a new group in the editor area.
*
* @param group the group to copy
* @param location the group from which to split to add the copied group
* @param direction the direction of where to split to
*/
copyGroup(group: IEditorGroup | GroupIdentifier, location: IEditorGroup | GroupIdentifier, direction: GroupDirection): IEditorGroup;
}
export enum GroupChangeKind {
/* Group Changes */
GROUP_ACTIVE,
GROUP_LABEL,
/* Editor Changes */
EDITOR_OPEN,
EDITOR_CLOSE,
EDITOR_MOVE,
EDITOR_ACTIVE,
EDITOR_LABEL,
EDITOR_PIN,
EDITOR_DIRTY
}
export interface IGroupChangeEvent {
kind: GroupChangeKind;
editor?: IEditorInput;
editorIndex?: number;
}
export interface IEditorGroup {
/**
* An aggregated event for when the group changes in any way.
*/
readonly onDidGroupChange: Event<IGroupChangeEvent>;
/**
* A unique identifier of this group that remains identical even if the
* group is moved to different locations.
*/
readonly id: GroupIdentifier;
/**
* A human readable label for the group. This label can change depending
* on the layout of all editor groups. Clients should listen on the
* `onDidGroupChange` event to react to that.
*/
readonly label: string;
/**
* The active control is the currently visible control of the group.
*/
readonly activeControl: IEditor;
/**
* The active editor is the currently visible editor of the group
* within the current active control.
*/
readonly activeEditor: IEditorInput;
/**
* The editor in the group that is in preview mode if any. There can
* only ever be one editor in preview mode.
*/
readonly previewEditor: IEditorInput;
/**
* The number of opend editors in this group.
*/
readonly count: number;
/**
* All opened editors in the group. There can only be one editor active.
*/
readonly editors: ReadonlyArray<IEditorInput>;
/**
* Returns the editor at a specific index of the group.
*/
getEditor(index: number): IEditorInput;
/**
* Get all editors that are currently opened in the group optionally
* sorted by being most recent active. Will sort by sequential appearance
* by default (from left to right).
*/
getEditors(order?: EditorsOrder): ReadonlyArray<IEditorInput>;
/**
* Returns the index of the editor in the group or -1 if not opened.
*/
getIndexOfEditor(editor: IEditorInput): number;
/**
* Open an editor in this group.
*
* @returns a promise that is resolved when the active editor (if any)
* has finished loading
*/
openEditor(editor: IEditorInput, options?: IEditorOptions | ITextEditorOptions): TPromise<void>;
/**
* Opens editors in this group.
*
* @returns a promise that is resolved when the active editor (if any)
* has finished loading
*/
openEditors(editors: IEditorInputWithOptions[]): TPromise<void>;
/**
* Find out if the provided editor is opened in the group.
*
* Note: An editor can be opened but not actively visible.
*/
isOpened(editor: IEditorInput): boolean;
/**
* Find out if the provided editor is pinned in the group.
*/
isPinned(editor: IEditorInput): boolean;
/**
* Find out if the provided editor is active in the group.
*/
isActive(editor: IEditorInput): boolean;
/**
* Move an editor from this group either within this group or to another group.
*/
moveEditor(editor: IEditorInput, target: IEditorGroup, options?: IMoveEditorOptions): void;
/**
* Copy an editor from this group to another group.
*
* Note: It is currently not supported to show the same editor more than once in the same group.
*/
copyEditor(editor: IEditorInput, target: IEditorGroup, options?: ICopyEditorOptions): void;
/**
* Close an editor from the group. This may trigger a confirmation dialog if
* the editor is dirty and thus returns a promise as value.
*
* @param editor the editor to close, or the currently active editor
* if unspecified.
*
* @returns a promise when the editor is closed.
*/
closeEditor(editor?: IEditorInput): TPromise<void>;
/**
* Closes specific editors in this group. This may trigger a confirmation dialog if
* there are dirty editors and thus returns a promise as value.
*
* @returns a promise when all editors are closed.
*/
closeEditors(editors: IEditorInput[] | ICloseEditorsFilter): TPromise<void>;
/**
* Closes all editors from the group. This may trigger a confirmation dialog if
* there are dirty editors and thus returns a promise as value.
*
* @returns a promise when all editors are closed.
*/
closeAllEditors(): TPromise<void>;
/**
* Replaces editors in this group with the provided replacement.
*
* @param editors the editors to replace
*
* @returns a promise that is resolved when the replaced active
* editor (if any) has finished loading.
*/
replaceEditors(editors: IEditorReplacement[]): TPromise<void>;
/**
* Set an editor to be pinned. A pinned editor is not replaced
* when another editor opens at the same location.
*
* @param editor the editor to pin, or the currently active editor
* if unspecified.
*/
pinEditor(editor?: IEditorInput): void;
/**
* Move keyboard focus into the group.
*/
focus(): void;
/**
* Invoke a function in the context of the services of this group.
*/
invokeWithinContext<T>(fn: (accessor: ServicesAccessor) => T): T;
}

View File

@@ -1,141 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { createDecorator, ServiceIdentifier, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { Position, IEditorInput } from 'vs/platform/editor/common/editor';
import { IEditorStacksModel, IEditorGroup, IEditorOpeningEvent } from 'vs/workbench/common/editor';
import { Event } from 'vs/base/common/event';
export enum GroupArrangement {
MINIMIZE_OTHERS,
EVEN
}
export type GroupOrientation = 'vertical' | 'horizontal';
export const IEditorGroupService = createDecorator<IEditorGroupService>('editorGroupService');
export interface IEditorTabOptions {
showTabs?: boolean;
tabCloseButton?: 'left' | 'right' | 'off';
tabSizing?: 'fit' | 'shrink';
showIcons?: boolean;
previewEditors?: boolean;
labelFormat?: 'default' | 'short' | 'medium' | 'long';
iconTheme?: string;
}
export interface IMoveOptions {
index?: number;
inactive?: boolean;
preserveFocus?: boolean;
}
/**
* The editor service allows to open editors and work on the active
* editor input and models.
*/
export interface IEditorGroupService {
_serviceBrand: ServiceIdentifier<any>;
/**
* Emitted when editors or inputs change. Examples: opening, closing of editors. Active editor change.
*/
onEditorsChanged: Event<void>;
/**
* Emitted when an editor is opening. Allows to prevent/replace the opening via the event method.
*/
onEditorOpening: Event<IEditorOpeningEvent>;
/**
* Emitted when opening an editor fails.
*/
onEditorOpenFail: Event<IEditorInput>;
/**
* Emitted when an entire editor group is moved to another position.
*/
onEditorGroupMoved: Event<void>;
/**
* Emitted when the editor group orientation was changed.
*/
onGroupOrientationChanged: Event<void>;
/**
* Emitted when tab options changed.
*/
onTabOptionsChanged: Event<IEditorTabOptions>;
/**
* Keyboard focus the editor group at the provided position.
*/
focusGroup(group: IEditorGroup): void;
focusGroup(position: Position): void;
/**
* Activate the editor group at the provided position without moving focus.
*/
activateGroup(group: IEditorGroup): void;
activateGroup(position: Position): void;
/**
* Allows to move the editor group from one position to another.
*/
moveGroup(from: IEditorGroup, to: IEditorGroup): void;
moveGroup(from: Position, to: Position): void;
/**
* Allows to arrange editor groups according to the GroupArrangement enumeration.
*/
arrangeGroups(arrangement: GroupArrangement): void;
/**
* Changes the editor group layout between vertical and horizontal orientation. Only applies
* if more than one editor is opened.
*/
setGroupOrientation(orientation: GroupOrientation): void;
/**
* Returns the current editor group layout.
*/
getGroupOrientation(): GroupOrientation;
/**
* Resize visible editor groups
*/
resizeGroup(position: Position, groupSizeChange: number): void;
/**
* Adds the pinned state to an editor, removing it from being a preview editor.
*/
pinEditor(group: IEditorGroup, input: IEditorInput): void;
pinEditor(position: Position, input: IEditorInput): void;
/**
* Moves an editor from one group to another. The index in the group is optional.
* The inactive option is applied when moving across groups.
*/
moveEditor(input: IEditorInput, from: IEditorGroup, to: IEditorGroup, moveOptions?: IMoveOptions): void;
moveEditor(input: IEditorInput, from: Position, to: Position, moveOptions?: IMoveOptions): void;
/**
* Provides access to the editor stacks model
*/
getStacksModel(): IEditorStacksModel;
/**
* Returns tab options.
*/
getTabOptions(): IEditorTabOptions;
/**
* Invoke a function in the context of the active editor.
*/
invokeWithinEditorContext<T>(fn: (accessor: ServicesAccessor) => T): T;
}

View File

@@ -0,0 +1,14 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as assert from 'assert';
suite('Editor groups service', () => {
test('groups basics', function () {
assert.equal(0, 0);
});
});

View File

@@ -5,7 +5,8 @@
'use strict';
import { createDecorator, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation';
import { IEditorInput, IResourceInput } from 'vs/platform/editor/common/editor';
import { IResourceInput } from 'vs/platform/editor/common/editor';
import { IEditorInput } from 'vs/workbench/common/editor';
import URI from 'vs/base/common/uri';
export const IHistoryService = createDecorator<IHistoryService>('historyService');

View File

@@ -9,28 +9,29 @@ import { TPromise } from 'vs/base/common/winjs.base';
import * as errors from 'vs/base/common/errors';
import URI from 'vs/base/common/uri';
import { IEditor } from 'vs/editor/common/editorCommon';
import { IEditor as IBaseEditor, IEditorInput, ITextEditorOptions, IResourceInput, ITextEditorSelection, Position as GroupPosition } from 'vs/platform/editor/common/editor';
import { Extensions as EditorExtensions, EditorInput, IEditorCloseEvent, IEditorGroup, IEditorInputFactoryRegistry, toResource, Extensions as EditorInputExtensions, IFileInputFactory } from 'vs/workbench/common/editor';
import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService';
import { ITextEditorOptions, IResourceInput, ITextEditorSelection } from 'vs/platform/editor/common/editor';
import { IEditorInput, IEditor as IBaseEditor, Extensions as EditorExtensions, EditorInput, IEditorCloseEvent, IEditorInputFactoryRegistry, toResource, Extensions as EditorInputExtensions, IFileInputFactory, IEditorIdentifier } from 'vs/workbench/common/editor';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IHistoryService } from 'vs/workbench/services/history/common/history';
import { FileChangesEvent, IFileService, FileChangeType, FILES_EXCLUDE_CONFIG } from 'vs/platform/files/common/files';
import { Selection } from 'vs/editor/common/core/selection';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { IDisposable, dispose, Disposable } from 'vs/base/common/lifecycle';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
import { Registry } from 'vs/platform/registry/common/platform';
import { once, debounceEvent } from 'vs/base/common/event';
import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration';
import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService';
import { IEditorGroupsService, IEditorGroup } from 'vs/workbench/services/group/common/editorGroupsService';
import { IWindowsService } from 'vs/platform/windows/common/windows';
import { getCodeEditor } from 'vs/editor/browser/services/codeEditorService';
import { getCodeEditor } from 'vs/editor/browser/editorBrowser';
import { getExcludes, ISearchConfiguration } from 'vs/platform/search/common/search';
import { IExpression } from 'vs/base/common/glob';
import { ICursorPositionChangedEvent } from 'vs/editor/common/controller/cursorEvents';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ResourceGlobMatcher } from 'vs/workbench/electron-browser/resources';
import { Schemas } from 'vs/base/common/network';
import { EditorServiceImpl } from 'vs/workbench/browser/parts/editor/editor';
/**
* Stores the selection & view state of an editor and allows to compare it to other selection states.
@@ -48,15 +49,15 @@ export class TextEditorState {
} : void 0;
}
public get editorInput(): IEditorInput {
get editorInput(): IEditorInput {
return this._editorInput;
}
public get selection(): ITextEditorSelection {
get selection(): ITextEditorSelection {
return this.textEditorSelection;
}
public justifiesNewPushState(other: TextEditorState, event?: ICursorPositionChangedEvent): boolean {
justifiesNewPushState(other: TextEditorState, event?: ICursorPositionChangedEvent): boolean {
if (event && event.source === 'api') {
return true; // always let API source win (e.g. "Go to definition" should add a history entry)
}
@@ -85,11 +86,6 @@ interface ISerializedEditorHistoryEntry {
editorInputJSON?: { typeId: string; deserialized: string; };
}
interface IEditorIdentifier {
editor: IEditorInput;
position: GroupPosition;
}
interface IStackEntry {
input: IEditorInput | IResourceInput;
selection?: ITextEditorSelection;
@@ -101,17 +97,15 @@ interface IRecentlyClosedFile {
index: number;
}
export class HistoryService implements IHistoryService {
export class HistoryService extends Disposable implements IHistoryService {
public _serviceBrand: any;
_serviceBrand: any;
private static readonly STORAGE_KEY = 'history.entries';
private static readonly MAX_HISTORY_ITEMS = 200;
private static readonly MAX_STACK_ITEMS = 20;
private static readonly MAX_RECENTLY_CLOSED_EDITORS = 20;
private toUnbind: IDisposable[];
private activeEditorListeners: IDisposable[];
private lastActiveEditor: IEditorIdentifier;
@@ -129,8 +123,8 @@ export class HistoryService implements IHistoryService {
private fileInputFactory: IFileInputFactory;
constructor(
@IWorkbenchEditorService private editorService: IWorkbenchEditorService,
@IEditorGroupService private editorGroupService: IEditorGroupService,
@IEditorService private editorService: EditorServiceImpl,
@IEditorGroupsService private editorGroupService: IEditorGroupsService,
@IWorkspaceContextService private contextService: IWorkspaceContextService,
@IStorageService private storageService: IStorageService,
@IConfigurationService private configurationService: IConfigurationService,
@@ -139,7 +133,8 @@ export class HistoryService implements IHistoryService {
@IWindowsService private windowService: IWindowsService,
@IInstantiationService private instantiationService: IInstantiationService,
) {
this.toUnbind = [];
super();
this.activeEditorListeners = [];
this.fileInputFactory = Registry.as<IEditorInputFactoryRegistry>(EditorInputExtensions.EditorInputFactories).getFileInputFactory();
@@ -149,11 +144,11 @@ export class HistoryService implements IHistoryService {
this.stack = [];
this.recentlyClosedFiles = [];
this.loaded = false;
this.resourceFilter = instantiationService.createInstance(
this.resourceFilter = this._register(instantiationService.createInstance(
ResourceGlobMatcher,
(root: URI) => this.getExcludes(root),
(event: IConfigurationChangeEvent) => event.affectsConfiguration(FILES_EXCLUDE_CONFIG) || event.affectsConfiguration('search.exclude')
);
));
this.registerListeners();
}
@@ -165,39 +160,39 @@ export class HistoryService implements IHistoryService {
}
private registerListeners(): void {
this.toUnbind.push(this.editorGroupService.onEditorsChanged(() => this.onEditorsChanged()));
this.toUnbind.push(this.lifecycleService.onShutdown(reason => this.saveHistory()));
this.toUnbind.push(this.editorGroupService.onEditorOpenFail(editor => this.remove(editor)));
this.toUnbind.push(this.editorGroupService.getStacksModel().onEditorClosed(event => this.onEditorClosed(event)));
this.toUnbind.push(this.fileService.onFileChanges(e => this.onFileChanges(e)));
this.toUnbind.push(this.resourceFilter.onExpressionChange(() => this.handleExcludesChange()));
this._register(this.editorService.onDidActiveEditorChange(() => this.onActiveEditorChanged()));
this._register(this.editorService.onDidOpenEditorFail(event => this.remove(event.editor)));
this._register(this.editorService.onDidCloseEditor(event => this.onEditorClosed(event)));
this._register(this.lifecycleService.onShutdown(reason => this.saveHistory()));
this._register(this.fileService.onFileChanges(event => this.onFileChanges(event)));
this._register(this.resourceFilter.onExpressionChange(() => this.handleExcludesChange()));
}
private onEditorsChanged(): void {
const activeEditor = this.editorService.getActiveEditor();
if (this.lastActiveEditor && this.matchesEditor(this.lastActiveEditor, activeEditor)) {
private onActiveEditorChanged(): void {
const activeControl = this.editorService.activeControl;
if (this.lastActiveEditor && this.matchesEditor(this.lastActiveEditor, activeControl)) {
return; // return if the active editor is still the same
}
// Remember as last active editor (can be undefined if none opened)
this.lastActiveEditor = activeEditor ? { editor: activeEditor.input, position: activeEditor.position } : void 0;
this.lastActiveEditor = activeControl ? { editor: activeControl.input, groupId: activeControl.group.id } : void 0;
// Dispose old listeners
dispose(this.activeEditorListeners);
this.activeEditorListeners = [];
// Propagate to history
this.handleActiveEditorChange(activeEditor);
this.handleActiveEditorChange(activeControl);
// Apply listener for selection changes if this is a text editor
const control = getCodeEditor(activeEditor);
if (control) {
const activeTextEditorWidget = getCodeEditor(this.editorService.activeTextEditorWidget);
if (activeTextEditorWidget) {
// Debounce the event with a timeout of 0ms so that multiple calls to
// editor.setSelection() are folded into one. We do not want to record
// subsequent history navigations for such API calls.
this.activeEditorListeners.push(debounceEvent(control.onDidChangeCursorPosition, (last, event) => event, 0)((event => {
this.handleEditorSelectionChangeEvent(activeEditor, event);
this.activeEditorListeners.push(debounceEvent(activeTextEditorWidget.onDidChangeCursorPosition, (last, event) => event, 0)((event => {
this.handleEditorSelectionChangeEvent(activeControl, event);
})));
}
}
@@ -207,7 +202,7 @@ export class HistoryService implements IHistoryService {
return false;
}
if (identifier.position !== editor.position) {
if (identifier.groupId !== editor.group.id) {
return false;
}
@@ -240,13 +235,11 @@ export class HistoryService implements IHistoryService {
}
}
public reopenLastClosedEditor(): void {
reopenLastClosedEditor(): void {
this.ensureHistoryLoaded();
const stacks = this.editorGroupService.getStacksModel();
let lastClosedFile = this.recentlyClosedFiles.pop();
while (lastClosedFile && this.isFileOpened(lastClosedFile.resource, stacks.activeGroup)) {
while (lastClosedFile && this.isFileOpened(lastClosedFile.resource, this.editorGroupService.activeGroup)) {
lastClosedFile = this.recentlyClosedFiles.pop(); // pop until we find a file that is not opened
}
@@ -255,7 +248,7 @@ export class HistoryService implements IHistoryService {
}
}
public forward(acrossEditors?: boolean): void {
forward(acrossEditors?: boolean): void {
if (this.stack.length > this.index + 1) {
if (acrossEditors) {
this.doForwardAcrossEditors();
@@ -293,7 +286,7 @@ export class HistoryService implements IHistoryService {
}
}
public back(acrossEditors?: boolean): void {
back(acrossEditors?: boolean): void {
if (this.index > 0) {
if (acrossEditors) {
this.doBackAcrossEditors();
@@ -303,7 +296,7 @@ export class HistoryService implements IHistoryService {
}
}
public last(): void {
last(): void {
if (this.lastIndex === -1) {
this.back();
} else {
@@ -335,7 +328,7 @@ export class HistoryService implements IHistoryService {
}
}
public clear(): void {
clear(): void {
this.ensureHistoryLoaded();
this.index = -1;
@@ -427,9 +420,9 @@ export class HistoryService implements IHistoryService {
this.removeExcludedFromHistory();
}
public remove(input: IEditorInput | IResourceInput): void;
public remove(input: FileChangesEvent): void;
public remove(arg1: IEditorInput | IResourceInput | FileChangesEvent): void {
remove(input: IEditorInput | IResourceInput): void;
remove(input: FileChangesEvent): void;
remove(arg1: IEditorInput | IResourceInput | FileChangesEvent): void {
this.removeFromHistory(arg1);
this.removeFromStack(arg1);
this.removeFromRecentlyClosedFiles(arg1);
@@ -448,16 +441,16 @@ export class HistoryService implements IHistoryService {
this.history = this.history.filter(e => !this.matches(arg1, e));
}
private handleEditorEventInStack(editor: IBaseEditor, event?: ICursorPositionChangedEvent): void {
const control = getCodeEditor(editor);
private handleEditorEventInStack(control: IBaseEditor, event?: ICursorPositionChangedEvent): void {
const codeEditor = control ? getCodeEditor(control.getControl()) : void 0;
// treat editor changes that happen as part of stack navigation specially
// we do not want to add a new stack entry as a matter of navigating the
// stack but we need to keep our currentTextEditorState up to date with
// the navigtion that occurs.
if (this.navigatingInStack) {
if (control && editor.input) {
this.currentTextEditorState = new TextEditorState(editor.input, control.getSelection());
if (codeEditor && control.input) {
this.currentTextEditorState = new TextEditorState(control.input, codeEditor.getSelection());
} else {
this.currentTextEditorState = null; // we navigated to a non text editor
}
@@ -467,16 +460,16 @@ export class HistoryService implements IHistoryService {
else {
// navigation inside text editor
if (control && editor.input) {
this.handleTextEditorEvent(editor, control, event);
if (codeEditor && control.input) {
this.handleTextEditorEvent(control, codeEditor, event);
}
// navigation to non-text editor
else {
this.currentTextEditorState = null; // at this time we have no active text editor view state
if (editor && editor.input) {
this.handleNonTextEditorEvent(editor);
if (control && control.input) {
this.handleNonTextEditorEvent(control);
}
}
}
@@ -508,7 +501,7 @@ export class HistoryService implements IHistoryService {
this.add(editor.input);
}
public add(input: IEditorInput, selection?: ITextEditorSelection): void {
add(input: IEditorInput, selection?: ITextEditorSelection): void {
if (!this.navigatingInStack) {
this.addOrReplaceInStack(input, selection);
}
@@ -621,11 +614,11 @@ export class HistoryService implements IHistoryService {
return false;
}
if (!group.contains(resource)) {
if (!this.editorService.isOpen({ resource }, group)) {
return false; // fast check
}
return group.getEditors().some(e => this.matchesFile(resource, e));
return group.editors.some(e => this.matchesFile(resource, e));
}
private matches(arg1: IEditorInput | IResourceInput | FileChangesEvent, inputB: IEditorInput | IResourceInput): boolean {
@@ -673,7 +666,7 @@ export class HistoryService implements IHistoryService {
return resourceInput && resourceInput.resource.toString() === resource.toString();
}
public getHistory(): (IEditorInput | IResourceInput)[] {
getHistory(): (IEditorInput | IResourceInput)[] {
this.ensureHistoryLoaded();
return this.history.slice(0);
@@ -754,7 +747,7 @@ export class HistoryService implements IHistoryService {
}).filter(input => !!input);
}
public getLastActiveWorkspaceRoot(schemeFilter?: string): URI {
getLastActiveWorkspaceRoot(schemeFilter?: string): URI {
// No Folder: return early
const folders = this.contextService.getWorkspace().folders;
@@ -802,7 +795,7 @@ export class HistoryService implements IHistoryService {
return void 0;
}
public getLastActiveFile(): URI {
getLastActiveFile(): URI {
const history = this.getHistory();
for (let i = 0; i < history.length; i++) {
let resource: URI;
@@ -821,8 +814,4 @@ export class HistoryService implements IHistoryService {
return void 0;
}
public dispose(): void {
this.toUnbind = dispose(this.toUnbind);
}
}

View File

@@ -8,7 +8,7 @@
import { IssueReporterStyles, IIssueService, IssueReporterData } from 'vs/platform/issue/common/issue';
import { TPromise } from 'vs/base/common/winjs.base';
import { ITheme, IThemeService } from 'vs/platform/theme/common/themeService';
import { textLinkForeground, inputBackground, inputBorder, inputForeground, buttonBackground, buttonHoverBackground, buttonForeground, inputValidationErrorBorder, foreground, inputActiveOptionBorder, scrollbarSliderActiveBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground, editorBackground, editorForeground, listHoverBackground, listHoverForeground, listHighlightForeground } from 'vs/platform/theme/common/colorRegistry';
import { textLinkForeground, inputBackground, inputBorder, inputForeground, buttonBackground, buttonHoverBackground, buttonForeground, inputValidationErrorBorder, foreground, inputActiveOptionBorder, scrollbarSliderActiveBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground, editorBackground, editorForeground, listHoverBackground, listHoverForeground, listHighlightForeground, textLinkActiveForeground } from 'vs/platform/theme/common/colorRegistry';
import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme';
import { IExtensionManagementService, IExtensionEnablementService, LocalExtensionType } from 'vs/platform/extensionManagement/common/extensionManagement';
import { webFrame } from 'electron';
@@ -63,6 +63,7 @@ export function getIssueReporterStyles(theme: ITheme): IssueReporterStyles {
backgroundColor: theme.getColor(SIDE_BAR_BACKGROUND) && theme.getColor(SIDE_BAR_BACKGROUND).toString(),
color: theme.getColor(foreground).toString(),
textLinkColor: theme.getColor(textLinkForeground) && theme.getColor(textLinkForeground).toString(),
textLinkActiveForeground: theme.getColor(textLinkActiveForeground) && theme.getColor(textLinkActiveForeground).toString(),
inputBackground: theme.getColor(inputBackground) && theme.getColor(inputBackground).toString(),
inputForeground: theme.getColor(inputForeground) && theme.getColor(inputForeground).toString(),
inputBorder: theme.getColor(inputBorder) && theme.getColor(inputBorder).toString(),

View File

@@ -6,9 +6,8 @@
import * as nls from 'vs/nls';
import { ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry';
import URI from 'vs/base/common/uri';
import * as strings from 'vs/base/common/strings';
import * as paths from 'vs/base/common/paths';
import * as resources from 'vs/base/common/resources';
interface IJSONValidationExtensionPoint {
fileMatch: string;
@@ -42,7 +41,7 @@ export class JSONValidationExtensionPoint {
for (let i = 0; i < extensions.length; i++) {
const extensionValue = <IJSONValidationExtensionPoint[]>extensions[i].value;
const collector = extensions[i].collector;
const extensionPath = extensions[i].description.extensionFolderPath;
const extensionLocation = extensions[i].description.extensionLocation;
if (!extensionValue || !Array.isArray(extensionValue)) {
collector.error(nls.localize('invalid.jsonValidation', "'configuration.jsonValidation' must be a array"));
@@ -60,7 +59,10 @@ export class JSONValidationExtensionPoint {
}
if (strings.startsWith(uri, './')) {
try {
uri = URI.file(paths.normalize(paths.join(extensionPath, uri))).toString();
const colorThemeLocation = resources.joinPath(extensionLocation, uri);
if (colorThemeLocation.path.indexOf(extensionLocation.path) !== 0) {
collector.warn(nls.localize('invalid.path.1', "Expected `contributes.{0}.url` ({1}) to be included inside extension's folder ({2}). This might make the extension non-portable.", configurationExtPoint.name, location, extensionLocation.path));
}
} catch (e) {
collector.error(nls.localize('invalid.url.fileschema', "'configuration.jsonValidation.url' is an invalid relative URL: {0}", e.message));
}

View File

@@ -74,10 +74,11 @@ export class KeybindingsEditingService extends Disposable implements IKeybinding
return this.resolveAndValidate()
.then(reference => {
const model = reference.object.textEditorModel;
if (keybindingItem.isDefault) {
this.updateDefaultKeybinding(key, keybindingItem, model);
} else {
this.updateUserKeybinding(key, keybindingItem, model);
const userKeybindingEntries = <IUserFriendlyKeybinding[]>json.parse(model.getValue());
const userKeybindingEntryIndex = this.findUserKeybindingEntryIndex(keybindingItem, userKeybindingEntries);
this.updateKeybinding(key, keybindingItem, model, userKeybindingEntryIndex);
if (keybindingItem.isDefault && keybindingItem.resolvedKeybinding) {
this.removeDefaultKeybinding(keybindingItem, model);
}
return this.save().then(() => reference.dispose());
});
@@ -112,32 +113,16 @@ export class KeybindingsEditingService extends Disposable implements IKeybinding
return this.textFileService.save(this.resource);
}
private updateUserKeybinding(newKey: string, keybindingItem: ResolvedKeybindingItem, model: ITextModel): void {
private updateKeybinding(newKey: string, keybindingItem: ResolvedKeybindingItem, model: ITextModel, userKeybindingEntryIndex: number): void {
const { tabSize, insertSpaces } = model.getOptions();
const eol = model.getEOL();
const userKeybindingEntries = <IUserFriendlyKeybinding[]>json.parse(model.getValue());
const userKeybindingEntryIndex = this.findUserKeybindingEntryIndex(keybindingItem, userKeybindingEntries);
if (userKeybindingEntryIndex !== -1) {
this.applyEditsToBuffer(setProperty(model.getValue(), [userKeybindingEntryIndex, 'key'], newKey, { tabSize, insertSpaces, eol })[0], model);
}
}
private updateDefaultKeybinding(newKey: string, keybindingItem: ResolvedKeybindingItem, model: ITextModel): void {
const { tabSize, insertSpaces } = model.getOptions();
const eol = model.getEOL();
const userKeybindingEntries = <IUserFriendlyKeybinding[]>json.parse(model.getValue());
const userKeybindingEntryIndex = this.findUserKeybindingEntryIndex(keybindingItem, userKeybindingEntries);
if (userKeybindingEntryIndex !== -1) {
// Update the keybinding with new key
this.applyEditsToBuffer(setProperty(model.getValue(), [userKeybindingEntryIndex, 'key'], newKey, { tabSize, insertSpaces, eol })[0], model);
} else {
// Add the new keybinidng with new key
// Add the new keybinding with new key
this.applyEditsToBuffer(setProperty(model.getValue(), [-1], this.asObject(newKey, keybindingItem.command, keybindingItem.when, false), { tabSize, insertSpaces, eol })[0], model);
}
if (keybindingItem.resolvedKeybinding) {
// Unassign the default keybinding
this.applyEditsToBuffer(setProperty(model.getValue(), [-1], this.asObject(keybindingItem.resolvedKeybinding.getUserSettingsLabel(), keybindingItem.command, keybindingItem.when, true), { tabSize, insertSpaces, eol })[0], model);
}
}
private removeUserKeybinding(keybindingItem: ResolvedKeybindingItem, model: ITextModel): void {
@@ -160,8 +145,8 @@ export class KeybindingsEditingService extends Disposable implements IKeybinding
const { tabSize, insertSpaces } = model.getOptions();
const eol = model.getEOL();
const userKeybindingEntries = <IUserFriendlyKeybinding[]>json.parse(model.getValue());
const index = this.findUnassignedDefaultKeybindingEntryIndex(keybindingItem, userKeybindingEntries);
if (index !== -1) {
const indices = this.findUnassignedDefaultKeybindingEntryIndex(keybindingItem, userKeybindingEntries).reverse();
for (const index of indices) {
this.applyEditsToBuffer(setProperty(model.getValue(), [index], void 0, { tabSize, insertSpaces, eol })[0], model);
}
}
@@ -183,13 +168,14 @@ export class KeybindingsEditingService extends Disposable implements IKeybinding
return -1;
}
private findUnassignedDefaultKeybindingEntryIndex(keybindingItem: ResolvedKeybindingItem, userKeybindingEntries: IUserFriendlyKeybinding[]): number {
private findUnassignedDefaultKeybindingEntryIndex(keybindingItem: ResolvedKeybindingItem, userKeybindingEntries: IUserFriendlyKeybinding[]): number[] {
const indices = [];
for (let index = 0; index < userKeybindingEntries.length; index++) {
if (userKeybindingEntries[index].command === `-${keybindingItem.command}`) {
return index;
indices.push(index);
}
}
return -1;
return indices;
}
private asObject(key: string, command: string, when: ContextKeyExpr, negate: boolean): any {
@@ -255,7 +241,7 @@ export class KeybindingsEditingService extends Disposable implements IKeybinding
private parse(model: ITextModel): { result: IUserFriendlyKeybinding[], parseErrors: json.ParseError[] } {
const parseErrors: json.ParseError[] = [];
const result = json.parse(model.getValue(), parseErrors, { allowTrailingComma: true });
const result = json.parse(model.getValue(), parseErrors);
return { result, parseErrors };
}

View File

@@ -25,12 +25,12 @@ export class KeybindingIO {
let quotedSerializedKeybinding = JSON.stringify(item.resolvedKeybinding.getUserSettingsLabel());
out.write(`{ "key": ${rightPaddedString(quotedSerializedKeybinding + ',', 25)} "command": `);
let serializedWhen = item.when ? item.when.serialize() : '';
let quotedSerializedWhen = item.when ? JSON.stringify(item.when.serialize()) : '';
let quotedSerializeCommand = JSON.stringify(item.command);
if (serializedWhen.length > 0) {
if (quotedSerializedWhen.length > 0) {
out.write(`${quotedSerializeCommand},`);
out.writeLine();
out.write(` "when": "${serializedWhen}" `);
out.write(` "when": ${quotedSerializedWhen} `);
} else {
out.write(`${quotedSerializeCommand} `);
}
@@ -42,7 +42,7 @@ export class KeybindingIO {
const [firstPart, chordPart] = (typeof input.key === 'string' ? this._readUserBinding(input.key) : [null, null]);
const when = (typeof input.when === 'string' ? ContextKeyExpr.deserialize(input.when) : null);
const command = (typeof input.command === 'string' ? input.command : null);
const commandArgs = (typeof input.args !== 'undefined' ? input.args : null);
const commandArgs = (typeof input.args !== 'undefined' ? input.args : undefined);
return {
firstPart: firstPart,
chordPart: chordPart,

View File

@@ -21,7 +21,7 @@ export class CachedKeyboardMapper implements IKeyboardMapper {
private _actual: IKeyboardMapper;
private _cache: Map<string, ResolvedKeybinding[]>;
constructor(actual) {
constructor(actual: IKeyboardMapper) {
this._actual = actual;
this._cache = new Map<string, ResolvedKeybinding[]>();
}

View File

@@ -16,7 +16,7 @@ import { KeybindingResolver } from 'vs/platform/keybinding/common/keybindingReso
import { ICommandService } from 'vs/platform/commands/common/commands';
import { IKeybindingEvent, IUserFriendlyKeybinding, KeybindingSource, IKeyboardEvent } from 'vs/platform/keybinding/common/keybinding';
import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IKeybindingItem, KeybindingsRegistry, IKeybindingRule2, KeybindingRuleSource } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { IKeybindingItem, KeybindingsRegistry, IKeybindingRule2, KeybindingRuleSource, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { Registry } from 'vs/platform/registry/common/platform';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { keybindingsTelemetry } from 'vs/platform/telemetry/common/telemetryUtils';
@@ -291,8 +291,7 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService {
this._cachedResolver = null;
this._firstTimeComputingResolver = true;
this.userKeybindings = new ConfigWatcher(environmentService.appKeybindingsPath, { defaultConfig: [], onError: error => onUnexpectedError(error) });
this.toDispose.push(this.userKeybindings);
this.userKeybindings = this._register(new ConfigWatcher(environmentService.appKeybindingsPath, { defaultConfig: [], onError: error => onUnexpectedError(error) }));
keybindingsExtPoint.setHandler((extensions) => {
let commandAdded = false;
@@ -306,12 +305,12 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService {
}
});
this.toDispose.push(this.userKeybindings.onDidUpdateConfiguration(event => this.updateResolver({
this._register(this.userKeybindings.onDidUpdateConfiguration(event => this.updateResolver({
source: KeybindingSource.User,
keybindings: event.config
})));
this.toDispose.push(dom.addDisposableListener(windowElement, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => {
this._register(dom.addDisposableListener(windowElement, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => {
let keyEvent = new StandardKeyboardEvent(e);
let shouldPreventDefault = this._dispatch(keyEvent, keyEvent.target);
if (shouldPreventDefault) {
@@ -367,6 +366,10 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService {
return this._cachedResolver;
}
protected _documentHasFocus(): boolean {
return document.hasFocus();
}
private _resolveKeybindingItems(items: IKeybindingItem[], isDefault: boolean): ResolvedKeybindingItem[] {
let result: ResolvedKeybindingItem[] = [], resultLen = 0;
for (let i = 0, len = items.length; i < len; i++) {
@@ -482,9 +485,9 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService {
let weight: number;
if (isBuiltin) {
weight = KeybindingsRegistry.WEIGHT.builtinExtension(idx);
weight = KeybindingWeight.BuiltinExtension + idx;
} else {
weight = KeybindingsRegistry.WEIGHT.externalExtension(idx);
weight = KeybindingWeight.ExternalExtension + idx;
}
let desc = {

View File

@@ -16,7 +16,9 @@ import { TPromise } from 'vs/base/common/winjs.base';
import { KeyCode, SimpleKeybinding, ChordKeybinding } from 'vs/base/common/keyCodes';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import * as extfs from 'vs/base/node/extfs';
import { TestTextFileService, TestEditorGroupService, TestLifecycleService, TestBackupFileService, TestContextService, TestTextResourceConfigurationService, TestHashService, TestEnvironmentService, TestStorageService } from 'vs/workbench/test/workbenchTestServices';
import { TestTextFileService, TestLifecycleService, TestBackupFileService, TestContextService, TestTextResourceConfigurationService, TestHashService, TestEnvironmentService, TestStorageService, TestEditorGroupsService, TestEditorService, TestLogService } from 'vs/workbench/test/workbenchTestServices';
import { IEditorGroupsService } from 'vs/workbench/services/group/common/editorGroupsService';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService';
import { IWorkspaceContextService, Workspace, toWorkspaceFolders } from 'vs/platform/workspace/common/workspace';
import * as uuid from 'vs/base/common/uuid';
@@ -29,7 +31,6 @@ import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils';
import { IBackupFileService } from 'vs/workbench/services/backup/common/backup';
import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService';
import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock';
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
import { ITextModelService } from 'vs/editor/common/services/resolverService';
@@ -46,6 +47,7 @@ import { TestConfigurationService } from 'vs/platform/configuration/test/common/
import { IHashService } from 'vs/workbench/services/hash/common/hashService';
import { mkdirp } from 'vs/base/node/pfs';
import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService';
import { ILogService } from 'vs/platform/log/common/log';
interface Modifiers {
metaKey?: boolean;
@@ -54,7 +56,7 @@ interface Modifiers {
shiftKey?: boolean;
}
suite('Keybindings Editing', () => {
suite('KeybindingsEditing', () => {
let instantiationService: TestInstantiationService;
let testObject: KeybindingsEditingService;
@@ -67,7 +69,7 @@ suite('Keybindings Editing', () => {
instantiationService = new TestInstantiationService();
instantiationService.stub(IEnvironmentService, { appKeybindingsPath: keybindingsFile });
instantiationService.stub(IEnvironmentService, <IEnvironmentService>{ appKeybindingsPath: keybindingsFile, appSettingsPath: path.join(testDir, 'settings.json') });
instantiationService.stub(IConfigurationService, ConfigurationService);
instantiationService.stub(IConfigurationService, 'getValue', { 'eol': '\n' });
instantiationService.stub(IConfigurationService, 'onDidUpdateConfiguration', () => { });
@@ -77,9 +79,11 @@ suite('Keybindings Editing', () => {
instantiationService.stub(ILifecycleService, lifecycleService);
instantiationService.stub(IContextKeyService, <IContextKeyService>instantiationService.createInstance(MockContextKeyService));
instantiationService.stub(IHashService, new TestHashService());
instantiationService.stub(IEditorGroupService, new TestEditorGroupService());
instantiationService.stub(IEditorGroupsService, new TestEditorGroupsService());
instantiationService.stub(IEditorService, new TestEditorService());
instantiationService.stub(ITelemetryService, NullTelemetryService);
instantiationService.stub(IModeService, ModeServiceImpl);
instantiationService.stub(ILogService, new TestLogService());
instantiationService.stub(IModelService, instantiationService.createInstance(ModelServiceImpl));
instantiationService.stub(IFileService, new FileService(
new TestContextService(new Workspace(testDir, testDir, toWorkspaceFolders([{ path: testDir }]))),
@@ -218,6 +222,21 @@ suite('Keybindings Editing', () => {
.then(() => assert.deepEqual(getUserKeybindings(), []));
});
test('reset mulitple removed keybindings', () => {
writeToKeybindingsFile({ key: 'alt+c', command: '-b' });
writeToKeybindingsFile({ key: 'alt+shift+c', command: '-b' });
writeToKeybindingsFile({ key: 'escape', command: '-b' });
return testObject.resetKeybinding(aResolvedKeybindingItem({ command: 'b', isDefault: false }))
.then(() => assert.deepEqual(getUserKeybindings(), []));
});
test('add a new keybinding to unassigned keybinding', () => {
writeToKeybindingsFile({ key: 'alt+c', command: '-a' });
const expected: IUserFriendlyKeybinding[] = [{ key: 'alt+c', command: '-a' }, { key: 'shift+alt+c', command: 'a' }];
return testObject.editKeybinding('shift+alt+c', aResolvedKeybindingItem({ command: 'a', isDefault: false }))
.then(() => assert.deepEqual(getUserKeybindings(), expected));
});
function writeToKeybindingsFile(...keybindings: IUserFriendlyKeybinding[]) {
fs.writeFileSync(keybindingsFile, JSON.stringify(keybindings || []));
}

View File

@@ -6,19 +6,31 @@
import * as nls from 'vs/nls';
import { onUnexpectedError } from 'vs/base/common/errors';
import * as paths from 'vs/base/common/paths';
import * as resources from 'vs/base/common/resources';
import { TPromise } from 'vs/base/common/winjs.base';
import * as mime from 'vs/base/common/mime';
import { IFilesConfiguration, FILES_ASSOCIATIONS_CONFIG } from 'vs/platform/files/common/files';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { IExtensionPointUser, ExtensionMessageCollector, IExtensionPoint, ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry';
import { ModesRegistry } from 'vs/editor/common/modes/modesRegistry';
import { ILanguageExtensionPoint, IValidLanguageExtensionPoint } from 'vs/editor/common/services/modeService';
import { ILanguageExtensionPoint } from 'vs/editor/common/services/modeService';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ModeServiceImpl } from 'vs/editor/common/services/modeServiceImpl';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import URI from 'vs/base/common/uri';
export const languagesExtPoint: IExtensionPoint<ILanguageExtensionPoint[]> = ExtensionsRegistry.registerExtensionPoint<ILanguageExtensionPoint[]>('languages', [], {
export interface IRawLanguageExtensionPoint {
id: string;
extensions: string[];
filenames: string[];
filenamePatterns: string[];
firstLine: string;
aliases: string[];
mimetypes: string[];
configuration: string;
}
export const languagesExtPoint: IExtensionPoint<IRawLanguageExtensionPoint[]> = ExtensionsRegistry.registerExtensionPoint<IRawLanguageExtensionPoint[]>('languages', [], {
description: nls.localize('vscode.extension.contributes.languages', 'Contributes language declarations.'),
type: 'array',
items: {
@@ -92,8 +104,8 @@ export class WorkbenchModeServiceImpl extends ModeServiceImpl {
this._configurationService = configurationService;
this._extensionService = extensionService;
languagesExtPoint.setHandler((extensions: IExtensionPointUser<ILanguageExtensionPoint[]>[]) => {
let allValidLanguages: IValidLanguageExtensionPoint[] = [];
languagesExtPoint.setHandler((extensions: IExtensionPointUser<IRawLanguageExtensionPoint[]>[]) => {
let allValidLanguages: ILanguageExtensionPoint[] = [];
for (let i = 0, len = extensions.length; i < len; i++) {
let extension = extensions[i];
@@ -106,7 +118,10 @@ export class WorkbenchModeServiceImpl extends ModeServiceImpl {
for (let j = 0, lenJ = extension.value.length; j < lenJ; j++) {
let ext = extension.value[j];
if (isValidLanguageExtensionPoint(ext, extension.collector)) {
let configuration = (ext.configuration ? paths.join(extension.description.extensionFolderPath, ext.configuration) : ext.configuration);
let configuration: URI;
if (ext.configuration) {
configuration = resources.joinPath(extension.description.extensionLocation, ext.configuration);
}
allValidLanguages.push({
id: ext.id,
extensions: ext.extensions,
@@ -175,7 +190,7 @@ function isUndefinedOrStringArray(value: string[]): boolean {
return value.every(item => typeof item === 'string');
}
function isValidLanguageExtensionPoint(value: ILanguageExtensionPoint, collector: ExtensionMessageCollector): boolean {
function isValidLanguageExtensionPoint(value: IRawLanguageExtensionPoint, collector: ExtensionMessageCollector): boolean {
if (!value) {
collector.error(nls.localize('invalid.empty', "Empty value for `contributes.{0}`", languagesExtPoint.name));
return false;

View File

@@ -7,31 +7,22 @@
import { INotificationService, INotification, INotificationHandle, Severity, NotificationMessage, INotificationActions, IPromptChoice } from 'vs/platform/notification/common/notification';
import { INotificationsModel, NotificationsModel } from 'vs/workbench/common/notifications';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { dispose, Disposable } from 'vs/base/common/lifecycle';
import { TPromise } from 'vs/base/common/winjs.base';
import { Action } from 'vs/base/common/actions';
import { once } from 'vs/base/common/event';
export class NotificationService implements INotificationService {
export class NotificationService extends Disposable implements INotificationService {
public _serviceBrand: any;
_serviceBrand: any;
private _model: INotificationsModel;
private toDispose: IDisposable[];
private _model: INotificationsModel = this._register(new NotificationsModel());
constructor() {
this.toDispose = [];
const model = new NotificationsModel();
this.toDispose.push(model);
this._model = model;
}
public get model(): INotificationsModel {
get model(): INotificationsModel {
return this._model;
}
public info(message: NotificationMessage | NotificationMessage[]): void {
info(message: NotificationMessage | NotificationMessage[]): void {
if (Array.isArray(message)) {
message.forEach(m => this.info(m));
@@ -41,7 +32,7 @@ export class NotificationService implements INotificationService {
this.model.notify({ severity: Severity.Info, message });
}
public warn(message: NotificationMessage | NotificationMessage[]): void {
warn(message: NotificationMessage | NotificationMessage[]): void {
if (Array.isArray(message)) {
message.forEach(m => this.warn(m));
@@ -51,7 +42,7 @@ export class NotificationService implements INotificationService {
this.model.notify({ severity: Severity.Warning, message });
}
public error(message: NotificationMessage | NotificationMessage[]): void {
error(message: NotificationMessage | NotificationMessage[]): void {
if (Array.isArray(message)) {
message.forEach(m => this.error(m));
@@ -61,11 +52,11 @@ export class NotificationService implements INotificationService {
this.model.notify({ severity: Severity.Error, message });
}
public notify(notification: INotification): INotificationHandle {
notify(notification: INotification): INotificationHandle {
return this.model.notify(notification);
}
public prompt(severity: Severity, message: string, choices: IPromptChoice[], onCancel?: () => void): INotificationHandle {
prompt(severity: Severity, message: string, choices: IPromptChoice[], onCancel?: () => void): INotificationHandle {
let handle: INotificationHandle;
let choiceClicked = false;
@@ -109,8 +100,4 @@ export class NotificationService implements INotificationService {
return handle;
}
public dispose(): void {
this.toDispose = dispose(this.toDispose);
}
}

View File

@@ -7,6 +7,7 @@
import { TPromise } from 'vs/base/common/winjs.base';
import { createDecorator, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation';
import { Event } from 'vs/base/common/event';
import { MenuBarVisibility } from 'vs/platform/windows/common/windows';
export enum Parts {
ACTIVITYBAR_PART,
@@ -14,7 +15,8 @@ export enum Parts {
PANEL_PART,
EDITOR_PART,
STATUSBAR_PART,
TITLEBAR_PART
TITLEBAR_PART,
MENUBAR_PART
}
export enum Position {
@@ -109,6 +111,11 @@ export interface IPartService {
*/
getSideBarPosition(): Position;
/**
* Gets the current menubar visibility.
*/
getMenubarVisibility(): MenuBarVisibility;
/**
* Gets the current panel position. Note that the panel can be hidden too.
*/

View File

@@ -7,26 +7,22 @@ import * as network from 'vs/base/common/network';
import { TPromise } from 'vs/base/common/winjs.base';
import * as nls from 'vs/nls';
import URI from 'vs/base/common/uri';
import * as labels from 'vs/base/common/labels';
import * as strings from 'vs/base/common/strings';
import { Disposable } from 'vs/base/common/lifecycle';
import { Emitter } from 'vs/base/common/event';
import { EditorInput } from 'vs/workbench/common/editor';
import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService';
import { EditorInput, IEditor } from 'vs/workbench/common/editor';
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
import { IWorkspaceConfigurationService } from 'vs/workbench/services/configuration/common/configuration';
import { Position as EditorPosition, IEditor, IEditorOptions } from 'vs/platform/editor/common/editor';
import { IEditorOptions } from 'vs/platform/editor/common/editor';
import { ITextModel } from 'vs/editor/common/model';
import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService';
import { IFileService, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IPreferencesService, IPreferencesEditorModel, ISetting, getSettingsTargetName, FOLDER_SETTINGS_PATH, DEFAULT_SETTINGS_EDITOR_SETTING } from 'vs/workbench/services/preferences/common/preferences';
import { SettingsEditorModel, DefaultSettingsEditorModel, DefaultKeybindingsEditorModel, defaultKeybindingsContents, DefaultSettings, WorkspaceConfigurationEditorModel } from 'vs/workbench/services/preferences/common/preferencesModels';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { DefaultPreferencesEditorInput, PreferencesEditorInput, KeybindingsEditorInput } from 'vs/workbench/services/preferences/common/preferencesEditorInput';
import { DefaultPreferencesEditorInput, PreferencesEditorInput, KeybindingsEditorInput, SettingsEditor2Input } from 'vs/workbench/services/preferences/common/preferencesEditorInput';
import { ITextModelService } from 'vs/editor/common/services/resolverService';
import { getCodeEditor } from 'vs/editor/browser/services/codeEditorService';
import { EditOperation } from 'vs/editor/common/core/editOperation';
import { Position, IPosition } from 'vs/editor/common/core/position';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
@@ -35,8 +31,12 @@ import { IJSONEditingService } from 'vs/workbench/services/configuration/common/
import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
import { IModeService } from 'vs/editor/common/services/modeService';
import { parse } from 'vs/base/common/json';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { ICodeEditor, getCodeEditor } from 'vs/editor/browser/editorBrowser';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { assign } from 'vs/base/common/objects';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IEditorGroup, IEditorGroupsService, GroupDirection } from 'vs/workbench/services/group/common/editorGroupsService';
import { IUriDisplayService } from 'vs/platform/uriDisplay/common/uriDisplay';
const emptyEditableSettingsContent = '{\n}';
@@ -56,8 +56,8 @@ export class PreferencesService extends Disposable implements IPreferencesServic
private _defaultFolderSettingsContentModel: DefaultSettings;
constructor(
@IWorkbenchEditorService private editorService: IWorkbenchEditorService,
@IEditorGroupService private editorGroupService: IEditorGroupService,
@IEditorService private editorService: IEditorService,
@IEditorGroupsService private editorGroupService: IEditorGroupsService,
@IFileService private fileService: IFileService,
@IWorkspaceConfigurationService private configurationService: IWorkspaceConfigurationService,
@INotificationService private notificationService: INotificationService,
@@ -69,7 +69,8 @@ export class PreferencesService extends Disposable implements IPreferencesServic
@IKeybindingService keybindingService: IKeybindingService,
@IModelService private modelService: IModelService,
@IJSONEditingService private jsonEditingService: IJSONEditingService,
@IModeService private modeService: IModeService
@IModeService private modeService: IModeService,
@IUriDisplayService private uriDisplayService: IUriDisplayService
) {
super();
// The default keybindings.json updates based on keyboard layouts, so here we make sure
@@ -167,8 +168,12 @@ export class PreferencesService extends Disposable implements IPreferencesServic
return TPromise.wrap<IPreferencesEditorModel<any>>(null);
}
openRawDefaultSettings(): TPromise<void> {
return this.editorService.openEditor({ resource: this.defaultSettingsRawResource }, EditorPosition.ONE) as TPromise<any>;
openRawDefaultSettings(): TPromise<IEditor> {
return this.editorService.openEditor({ resource: this.defaultSettingsRawResource });
}
openRawUserSettings(): TPromise<IEditor> {
return this.editorService.openEditor({ resource: this.userSettingsResource });
}
openSettings(): TPromise<IEditor> {
@@ -178,26 +183,30 @@ export class PreferencesService extends Disposable implements IPreferencesServic
return this.openOrSwitchSettings(target, resource);
}
openGlobalSettings(options?: IEditorOptions, position?: EditorPosition): TPromise<IEditor> {
return this.openOrSwitchSettings(ConfigurationTarget.USER, this.userSettingsResource, options, position);
openGlobalSettings(options?: IEditorOptions, group?: IEditorGroup): TPromise<IEditor> {
return this.openOrSwitchSettings(ConfigurationTarget.USER, this.userSettingsResource, options, group);
}
openWorkspaceSettings(options?: IEditorOptions, position?: EditorPosition): TPromise<IEditor> {
openSettings2(): TPromise<IEditor> {
return this.editorService.openEditor(this.instantiationService.createInstance(SettingsEditor2Input), { pinned: true }).then(() => null);
}
openWorkspaceSettings(options?: IEditorOptions, group?: IEditorGroup): TPromise<IEditor> {
if (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) {
this.notificationService.info(nls.localize('openFolderFirst', "Open a folder first to create workspace settings"));
return TPromise.as(null);
}
return this.openOrSwitchSettings(ConfigurationTarget.WORKSPACE, this.workspaceSettingsResource, options, position);
return this.openOrSwitchSettings(ConfigurationTarget.WORKSPACE, this.workspaceSettingsResource, options, group);
}
openFolderSettings(folder: URI, options?: IEditorOptions, position?: EditorPosition): TPromise<IEditor> {
return this.openOrSwitchSettings(ConfigurationTarget.WORKSPACE_FOLDER, this.getEditableSettingsURI(ConfigurationTarget.WORKSPACE_FOLDER, folder), options, position);
openFolderSettings(folder: URI, options?: IEditorOptions, group?: IEditorGroup): TPromise<IEditor> {
return this.openOrSwitchSettings(ConfigurationTarget.WORKSPACE_FOLDER, this.getEditableSettingsURI(ConfigurationTarget.WORKSPACE_FOLDER, folder), options, group);
}
switchSettings(target: ConfigurationTarget, resource: URI): TPromise<void> {
const activeEditor = this.editorService.getActiveEditor();
if (activeEditor && activeEditor.input instanceof PreferencesEditorInput) {
return this.doSwitchSettings(target, resource, activeEditor.input, activeEditor.position).then(() => null);
const activeControl = this.editorService.activeControl;
if (activeControl && activeControl.input instanceof PreferencesEditorInput) {
return this.doSwitchSettings(target, resource, activeControl.input, activeControl.group).then(() => null);
} else {
return this.doOpenSettings(target, resource).then(() => null);
}
@@ -213,81 +222,94 @@ export class PreferencesService extends Disposable implements IPreferencesServic
if (textual) {
const emptyContents = '// ' + nls.localize('emptyKeybindingsHeader', "Place your key bindings in this file to overwrite the defaults") + '\n[\n]';
const editableKeybindings = URI.file(this.environmentService.appKeybindingsPath);
const openDefaultKeybindings = !!this.configurationService.getValue('workbench.settings.openDefaultKeybindings');
// Create as needed and open in editor
return this.createIfNotExists(editableKeybindings, emptyContents).then(() => {
return this.editorService.openEditors([
{ input: { resource: this.defaultKeybindingsResource, options: { pinned: true }, label: nls.localize('defaultKeybindings', "Default Keybindings"), description: '' }, position: EditorPosition.ONE },
{ input: { resource: editableKeybindings, options: { pinned: true } }, position: EditorPosition.TWO },
]).then(() => {
this.editorGroupService.focusGroup(EditorPosition.TWO);
});
if (openDefaultKeybindings) {
const activeEditorGroup = this.editorGroupService.activeGroup;
const sideEditorGroup = this.editorGroupService.addGroup(activeEditorGroup.id, GroupDirection.RIGHT);
return TPromise.join([
this.editorService.openEditor({ resource: this.defaultKeybindingsResource, options: { pinned: true, preserveFocus: true }, label: nls.localize('defaultKeybindings', "Default Keybindings"), description: '' }),
this.editorService.openEditor({ resource: editableKeybindings, options: { pinned: true } }, sideEditorGroup.id)
]).then(editors => void 0);
} else {
return this.editorService.openEditor({ resource: editableKeybindings, options: { pinned: true } }).then(() => void 0);
}
});
}
return this.editorService.openEditor(this.instantiationService.createInstance(KeybindingsEditorInput), { pinned: true }).then(() => null);
}
openDefaultKeybindingsFile(): TPromise<IEditor> {
return this.editorService.openEditor({ resource: this.defaultKeybindingsResource });
}
configureSettingsForLanguage(language: string): void {
this.openGlobalSettings()
.then(editor => {
const codeEditor = getCodeEditor(editor);
this.getPosition(language, codeEditor)
.then(position => {
codeEditor.setPosition(position);
codeEditor.focus();
});
});
.then(editor => this.createPreferencesEditorModel(this.userSettingsResource)
.then((settingsModel: IPreferencesEditorModel<ISetting>) => {
const codeEditor = getCodeEditor(editor.getControl());
if (codeEditor) {
this.getPosition(language, settingsModel, codeEditor)
.then(position => {
if (codeEditor) {
codeEditor.setPosition(position);
codeEditor.focus();
}
});
}
}));
}
private openOrSwitchSettings(configurationTarget: ConfigurationTarget, resource: URI, options?: IEditorOptions, position?: EditorPosition): TPromise<IEditor> {
const activeGroup = this.editorGroupService.getStacksModel().activeGroup;
const positionToReplace = position !== void 0 ? position : activeGroup ? this.editorGroupService.getStacksModel().positionOfGroup(activeGroup) : EditorPosition.ONE;
const editorInput = this.getActiveSettingsEditorInput(positionToReplace);
private openOrSwitchSettings(configurationTarget: ConfigurationTarget, resource: URI, options?: IEditorOptions, group: IEditorGroup = this.editorGroupService.activeGroup): TPromise<IEditor> {
const editorInput = this.getActiveSettingsEditorInput(group);
if (editorInput && editorInput.master.getResource().fsPath !== resource.fsPath) {
return this.doSwitchSettings(configurationTarget, resource, editorInput, positionToReplace);
return this.doSwitchSettings(configurationTarget, resource, editorInput, group);
}
return this.doOpenSettings(configurationTarget, resource, options, position);
return this.doOpenSettings(configurationTarget, resource, options, group);
}
private doOpenSettings(configurationTarget: ConfigurationTarget, resource: URI, options?: IEditorOptions, position?: EditorPosition): TPromise<IEditor> {
private doOpenSettings(configurationTarget: ConfigurationTarget, resource: URI, options?: IEditorOptions, group?: IEditorGroup): TPromise<IEditor> {
const openDefaultSettings = !!this.configurationService.getValue(DEFAULT_SETTINGS_EDITOR_SETTING);
return this.getOrCreateEditableSettingsEditorInput(configurationTarget, resource)
.then(editableSettingsEditorInput => {
if (!options) {
options = { pinned: true };
} else {
options.pinned = true;
options = assign(options, { pinned: true });
}
if (openDefaultSettings) {
const defaultPreferencesEditorInput = this.instantiationService.createInstance(DefaultPreferencesEditorInput, this.getDefaultSettingsResource(configurationTarget));
const preferencesEditorInput = new PreferencesEditorInput(this.getPreferencesEditorInputName(configurationTarget, resource), editableSettingsEditorInput.getDescription(), defaultPreferencesEditorInput, <EditorInput>editableSettingsEditorInput);
this.lastOpenedSettingsInput = preferencesEditorInput;
return this.editorService.openEditor(preferencesEditorInput, options, position);
return this.editorService.openEditor(preferencesEditorInput, options, group);
}
return this.editorService.openEditor(editableSettingsEditorInput, options, position);
return this.editorService.openEditor(editableSettingsEditorInput, options, group);
});
}
private doSwitchSettings(target: ConfigurationTarget, resource: URI, input: PreferencesEditorInput, position?: EditorPosition): TPromise<IEditor> {
private doSwitchSettings(target: ConfigurationTarget, resource: URI, input: PreferencesEditorInput, group: IEditorGroup): TPromise<IEditor> {
return this.getOrCreateEditableSettingsEditorInput(target, this.getEditableSettingsURI(target, resource))
.then(toInput => {
const replaceWith = new PreferencesEditorInput(this.getPreferencesEditorInputName(target, resource), toInput.getDescription(), this.instantiationService.createInstance(DefaultPreferencesEditorInput, this.getDefaultSettingsResource(target)), toInput);
return this.editorService.replaceEditors([{
toReplace: input,
replaceWith
}], position).then(editors => {
this.lastOpenedSettingsInput = replaceWith;
return editors[0];
return group.openEditor(input).then(() => {
const replaceWith = new PreferencesEditorInput(this.getPreferencesEditorInputName(target, resource), toInput.getDescription(), this.instantiationService.createInstance(DefaultPreferencesEditorInput, this.getDefaultSettingsResource(target)), toInput);
return group.replaceEditors([{
editor: input,
replacement: replaceWith
}]).then(() => {
this.lastOpenedSettingsInput = replaceWith;
return group.activeControl;
});
});
});
}
private getActiveSettingsEditorInput(position?: EditorPosition): PreferencesEditorInput {
const stacksModel = this.editorGroupService.getStacksModel();
const group = position !== void 0 ? stacksModel.groupAt(position) : stacksModel.activeGroup;
return group && <PreferencesEditorInput>group.getEditors().filter(e => e instanceof PreferencesEditorInput)[0];
private getActiveSettingsEditorInput(group: IEditorGroup = this.editorGroupService.activeGroup): PreferencesEditorInput {
return <PreferencesEditorInput>group.editors.filter(e => e instanceof PreferencesEditorInput)[0];
}
private getConfigurationTargetFromSettingsResource(resource: URI): ConfigurationTarget {
@@ -423,7 +445,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic
return this.fileService.resolveContent(resource, { acceptTextOnly: true }).then(null, error => {
if ((<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_NOT_FOUND) {
return this.fileService.updateContent(resource, contents).then(null, error => {
return TPromise.wrapError(new Error(nls.localize('fail.createSettings', "Unable to create '{0}' ({1}).", labels.getPathLabel(resource, this.contextService, this.environmentService), error)));
return TPromise.wrapError(new Error(nls.localize('fail.createSettings', "Unable to create '{0}' ({1}).", this.uriDisplayService.getLabel(resource, true), error)));
});
}
@@ -447,39 +469,36 @@ export class PreferencesService extends Disposable implements IPreferencesServic
];
}
private getPosition(language: string, codeEditor: ICodeEditor): TPromise<IPosition> {
return this.createPreferencesEditorModel(this.userSettingsResource)
.then((settingsModel: IPreferencesEditorModel<ISetting>) => {
const languageKey = `[${language}]`;
let setting = settingsModel.getPreference(languageKey);
const model = codeEditor.getModel();
const configuration = this.configurationService.getValue<{ editor: { tabSize: number; insertSpaces: boolean }, files: { eol: string } }>();
const eol = configuration.files && configuration.files.eol;
if (setting) {
if (setting.overrides.length) {
const lastSetting = setting.overrides[setting.overrides.length - 1];
let content;
if (lastSetting.valueRange.endLineNumber === setting.range.endLineNumber) {
content = ',' + eol + this.spaces(2, configuration.editor) + eol + this.spaces(1, configuration.editor);
} else {
content = ',' + eol + this.spaces(2, configuration.editor);
}
const editOperation = EditOperation.insert(new Position(lastSetting.valueRange.endLineNumber, lastSetting.valueRange.endColumn), content);
model.pushEditOperations([], [editOperation], () => []);
return { lineNumber: lastSetting.valueRange.endLineNumber + 1, column: model.getLineMaxColumn(lastSetting.valueRange.endLineNumber + 1) };
}
return { lineNumber: setting.valueRange.startLineNumber, column: setting.valueRange.startColumn + 1 };
private getPosition(language: string, settingsModel: IPreferencesEditorModel<ISetting>, codeEditor: ICodeEditor): TPromise<IPosition> {
const languageKey = `[${language}]`;
let setting = settingsModel.getPreference(languageKey);
const model = codeEditor.getModel();
const configuration = this.configurationService.getValue<{ editor: { tabSize: number; insertSpaces: boolean }, files: { eol: string } }>();
const eol = configuration.files && configuration.files.eol;
if (setting) {
if (setting.overrides.length) {
const lastSetting = setting.overrides[setting.overrides.length - 1];
let content;
if (lastSetting.valueRange.endLineNumber === setting.range.endLineNumber) {
content = ',' + eol + this.spaces(2, configuration.editor) + eol + this.spaces(1, configuration.editor);
} else {
content = ',' + eol + this.spaces(2, configuration.editor);
}
return this.configurationService.updateValue(languageKey, {}, ConfigurationTarget.USER)
.then(() => {
setting = settingsModel.getPreference(languageKey);
let content = eol + this.spaces(2, configuration.editor) + eol + this.spaces(1, configuration.editor);
let editOperation = EditOperation.insert(new Position(setting.valueRange.endLineNumber, setting.valueRange.endColumn - 1), content);
model.pushEditOperations([], [editOperation], () => []);
let lineNumber = setting.valueRange.endLineNumber + 1;
settingsModel.dispose();
return { lineNumber, column: model.getLineMaxColumn(lineNumber) };
});
const editOperation = EditOperation.insert(new Position(lastSetting.valueRange.endLineNumber, lastSetting.valueRange.endColumn), content);
model.pushEditOperations([], [editOperation], () => []);
return TPromise.as({ lineNumber: lastSetting.valueRange.endLineNumber + 1, column: model.getLineMaxColumn(lastSetting.valueRange.endLineNumber + 1) });
}
return TPromise.as({ lineNumber: setting.valueRange.startLineNumber, column: setting.valueRange.startColumn + 1 });
}
return this.configurationService.updateValue(languageKey, {}, ConfigurationTarget.USER)
.then(() => {
setting = settingsModel.getPreference(languageKey);
let content = eol + this.spaces(2, configuration.editor) + eol + this.spaces(1, configuration.editor);
let editOperation = EditOperation.insert(new Position(setting.valueRange.endLineNumber, setting.valueRange.endColumn - 1), content);
model.pushEditOperations([], [editOperation], () => []);
let lineNumber = setting.valueRange.endLineNumber + 1;
settingsModel.dispose();
return { lineNumber, column: model.getLineMaxColumn(lineNumber) };
});
}

View File

@@ -261,13 +261,15 @@ class KeybindingItemMatches {
public readonly whenMatches: IMatch[] = null;
public readonly keybindingMatches: KeybindingMatches = null;
constructor(private modifierLabels: ModifierLabels, keybindingItem: IKeybindingItem, searchValue: string, words: string[], keybindingWords: string[], private completeMatch: boolean) {
this.commandIdMatches = this.matches(searchValue, keybindingItem.command, or(matchesWords, matchesCamelCase), words);
this.commandLabelMatches = keybindingItem.commandLabel ? this.matches(searchValue, keybindingItem.commandLabel, (word, wordToMatchAgainst) => matchesWords(word, keybindingItem.commandLabel, true), words) : null;
this.commandDefaultLabelMatches = keybindingItem.commandDefaultLabel ? this.matches(searchValue, keybindingItem.commandDefaultLabel, (word, wordToMatchAgainst) => matchesWords(word, keybindingItem.commandDefaultLabel, true), words) : null;
this.sourceMatches = this.matches(searchValue, keybindingItem.source, (word, wordToMatchAgainst) => matchesWords(word, keybindingItem.source, true), words);
this.whenMatches = keybindingItem.when ? this.matches(searchValue, keybindingItem.when, or(matchesWords, matchesCamelCase), words) : null;
this.keybindingMatches = keybindingItem.keybinding ? this.matchesKeybinding(keybindingItem.keybinding, searchValue, keybindingWords) : null;
constructor(private modifierLabels: ModifierLabels, keybindingItem: IKeybindingItem, searchValue: string, words: string[], keybindingWords: string[], completeMatch: boolean) {
if (!completeMatch) {
this.commandIdMatches = this.matches(searchValue, keybindingItem.command, or(matchesWords, matchesCamelCase), words);
this.commandLabelMatches = keybindingItem.commandLabel ? this.matches(searchValue, keybindingItem.commandLabel, (word, wordToMatchAgainst) => matchesWords(word, keybindingItem.commandLabel, true), words) : null;
this.commandDefaultLabelMatches = keybindingItem.commandDefaultLabel ? this.matches(searchValue, keybindingItem.commandDefaultLabel, (word, wordToMatchAgainst) => matchesWords(word, keybindingItem.commandDefaultLabel, true), words) : null;
this.sourceMatches = this.matches(searchValue, keybindingItem.source, (word, wordToMatchAgainst) => matchesWords(word, keybindingItem.source, true), words);
this.whenMatches = keybindingItem.when ? this.matches(searchValue, keybindingItem.when, or(matchesWords, matchesCamelCase), words) : null;
}
this.keybindingMatches = keybindingItem.keybinding ? this.matchesKeybinding(keybindingItem.keybinding, searchValue, keybindingWords, completeMatch) : null;
}
private matches(searchValue: string, wordToMatchAgainst: string, wordMatchesFilter: IFilter, words: string[]): IMatch[] {
@@ -299,7 +301,7 @@ class KeybindingItemMatches {
return distinct(matches, (a => a.start + '.' + a.end)).filter(match => !matches.some(m => !(m.start === match.start && m.end === match.end) && (m.start <= match.start && m.end >= match.end))).sort((a, b) => a.start - b.start);
}
private matchesKeybinding(keybinding: ResolvedKeybinding, searchValue: string, words: string[]): KeybindingMatches {
private matchesKeybinding(keybinding: ResolvedKeybinding, searchValue: string, words: string[], completeMatch: boolean): KeybindingMatches {
const [firstPart, chordPart] = keybinding.getParts();
if (strings.compareIgnoreCase(searchValue, keybinding.getAriaLabel()) === 0 || strings.compareIgnoreCase(searchValue, keybinding.getLabel()) === 0) {
@@ -325,7 +327,7 @@ class KeybindingItemMatches {
let matchChordPart = !chordPartMatch.keyCode;
if (matchFirstPart) {
firstPartMatched = this.matchPart(firstPart, firstPartMatch, word);
firstPartMatched = this.matchPart(firstPart, firstPartMatch, word, completeMatch);
if (firstPartMatch.keyCode) {
for (const cordPartMatchedWordIndex of chordPartMatchedWords) {
if (firstPartMatchedWords.indexOf(cordPartMatchedWordIndex) === -1) {
@@ -339,7 +341,7 @@ class KeybindingItemMatches {
}
if (matchChordPart) {
chordPartMatched = this.matchPart(chordPart, chordPartMatch, word);
chordPartMatched = this.matchPart(chordPart, chordPartMatch, word, completeMatch);
}
if (firstPartMatched) {
@@ -357,13 +359,13 @@ class KeybindingItemMatches {
if (matchedWords.length !== words.length) {
return null;
}
if (this.completeMatch && (!this.isCompleteMatch(firstPart, firstPartMatch) || !this.isCompleteMatch(chordPart, chordPartMatch))) {
if (completeMatch && (!this.isCompleteMatch(firstPart, firstPartMatch) || !this.isCompleteMatch(chordPart, chordPartMatch))) {
return null;
}
return this.hasAnyMatch(firstPartMatch) || this.hasAnyMatch(chordPartMatch) ? { firstPart: firstPartMatch, chordPart: chordPartMatch } : null;
}
private matchPart(part: ResolvedKeybindingPart, match: KeybindingMatch, word: string): boolean {
private matchPart(part: ResolvedKeybindingPart, match: KeybindingMatch, word: string, completeMatch: boolean): boolean {
let matched = false;
if (this.matchesMetaModifier(part, word)) {
matched = true;
@@ -381,19 +383,19 @@ class KeybindingItemMatches {
matched = true;
match.altKey = true;
}
if (this.matchesKeyCode(part, word)) {
if (this.matchesKeyCode(part, word, completeMatch)) {
match.keyCode = true;
matched = true;
}
return matched;
}
private matchesKeyCode(keybinding: ResolvedKeybindingPart, word: string): boolean {
private matchesKeyCode(keybinding: ResolvedKeybindingPart, word: string, completeMatch: boolean): boolean {
if (!keybinding) {
return false;
}
const ariaLabel = keybinding.keyAriaLabel;
if (this.completeMatch || ariaLabel.length === 1 || word.length === 1) {
if (completeMatch || ariaLabel.length === 1 || word.length === 1) {
if (strings.compareIgnoreCase(ariaLabel, word) === 0) {
return true;
}

View File

@@ -6,7 +6,8 @@
import URI from 'vs/base/common/uri';
import { TPromise } from 'vs/base/common/winjs.base';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IEditor, Position, IEditorOptions } from 'vs/platform/editor/common/editor';
import { IEditorOptions } from 'vs/platform/editor/common/editor';
import { IEditor } from 'vs/workbench/common/editor';
import { ITextModel } from 'vs/editor/common/model';
import { IRange } from 'vs/editor/common/core/range';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
@@ -16,6 +17,7 @@ import { Event } from 'vs/base/common/event';
import { IStringDictionary } from 'vs/base/common/collections';
import { ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement';
import { localize } from 'vs/nls';
import { IEditorGroup } from 'vs/workbench/services/group/common/editorGroupsService';
export interface ISettingsGroup {
id: string;
@@ -23,6 +25,7 @@ export interface ISettingsGroup {
title: string;
titleRange: IRange;
sections: ISettingsSection[];
contributedByExtension: boolean;
}
export interface ISettingsSection {
@@ -41,6 +44,12 @@ export interface ISetting {
descriptionRanges: IRange[];
overrides?: ISetting[];
overrideOf?: ISetting;
// TODO@roblou maybe need new type and new EditorModel for GUI editor instead of ISetting which is used for text settings editor
type?: string | string[];
enum?: string[];
enumDescriptions?: string[];
tags?: string[];
}
export interface IExtensionSetting extends ISetting {
@@ -50,6 +59,7 @@ export interface IExtensionSetting extends ISetting {
export interface ISearchResult {
filterMatches: ISettingMatch[];
exactMatch?: boolean;
metadata?: IFilterMetadata;
}
@@ -66,6 +76,7 @@ export interface IFilterResult {
allGroups: ISettingsGroup[];
matches: IRange[];
metadata?: IStringDictionary<IFilterMetadata>;
exactMatch?: boolean;
}
export interface ISettingMatch {
@@ -136,13 +147,15 @@ export interface IPreferencesService {
resolveModel(uri: URI): TPromise<ITextModel>;
createPreferencesEditorModel<T>(uri: URI): TPromise<IPreferencesEditorModel<T>>;
openRawDefaultSettings(): TPromise<void>;
openRawDefaultSettings(): TPromise<IEditor>;
openSettings(): TPromise<IEditor>;
openGlobalSettings(options?: IEditorOptions, position?: Position): TPromise<IEditor>;
openWorkspaceSettings(options?: IEditorOptions, position?: Position): TPromise<IEditor>;
openFolderSettings(folder: URI, options?: IEditorOptions, position?: Position): TPromise<IEditor>;
openSettings2(): TPromise<IEditor>;
openGlobalSettings(options?: IEditorOptions, group?: IEditorGroup): TPromise<IEditor>;
openWorkspaceSettings(options?: IEditorOptions, group?: IEditorGroup): TPromise<IEditor>;
openFolderSettings(folder: URI, options?: IEditorOptions, group?: IEditorGroup): TPromise<IEditor>;
switchSettings(target: ConfigurationTarget, resource: URI): TPromise<void>;
openGlobalKeybindingSettings(textual: boolean): TPromise<void>;
openDefaultKeybindingsFile(): TPromise<IEditor>;
configureSettingsForLanguage(language: string): void;
}

View File

@@ -3,17 +3,18 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls';
import { SideBySideEditorInput, EditorInput } from 'vs/workbench/common/editor';
import { Verbosity } from 'vs/platform/editor/common/editor';
import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput';
import { OS } from 'vs/base/common/platform';
import URI from 'vs/base/common/uri';
import { TPromise } from 'vs/base/common/winjs.base';
import { ITextModelService } from 'vs/editor/common/services/resolverService';
import * as nls from 'vs/nls';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { EditorInput, SideBySideEditorInput, Verbosity } from 'vs/workbench/common/editor';
import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput';
import { IHashService } from 'vs/workbench/services/hash/common/hashService';
import { KeybindingsEditorModel } from 'vs/workbench/services/preferences/common/keybindingsEditorModel';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { OS } from 'vs/base/common/platform';
import { TPromise } from 'vs/base/common/winjs.base';
import { IPreferencesService } from './preferences';
import { DefaultSettingsEditorModel } from './preferencesModels';
export class PreferencesEditorInput extends SideBySideEditorInput {
public static readonly ID: string = 'workbench.editorinputs.preferencesEditorInput';
@@ -22,10 +23,6 @@ export class PreferencesEditorInput extends SideBySideEditorInput {
return PreferencesEditorInput.ID;
}
public supportsSplitEditor(): boolean {
return true;
}
public getTitle(verbosity: Verbosity): string {
return this.master.getTitle(verbosity);
}
@@ -73,7 +70,7 @@ export class KeybindingsEditorInput extends EditorInput {
return nls.localize('keybindingsInputName', "Keyboard Shortcuts");
}
resolve(refresh?: boolean): TPromise<KeybindingsEditorModel> {
resolve(): TPromise<KeybindingsEditorModel> {
return TPromise.as(this.keybindingsModel);
}
@@ -81,3 +78,30 @@ export class KeybindingsEditorInput extends EditorInput {
return otherInput instanceof KeybindingsEditorInput;
}
}
export class SettingsEditor2Input extends EditorInput {
public static readonly ID: string = 'workbench.input.settings2';
constructor(
@IPreferencesService private preferencesService: IPreferencesService
) {
super();
}
getTypeId(): string {
return SettingsEditor2Input.ID;
}
getName(): string {
return nls.localize('settingsEditor2InputName', "Settings (Preview)");
}
resolve(): TPromise<DefaultSettingsEditorModel> {
return <TPromise<DefaultSettingsEditorModel>>this.preferencesService.createPreferencesEditorModel(URI.parse('vscode://defaultsettings/0/settings.json'));
}
matches(otherInput: any): boolean {
return otherInput instanceof SettingsEditor2Input;
}
}

View File

@@ -3,25 +3,25 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls';
import { assign } from 'vs/base/common/objects';
import * as map from 'vs/base/common/map';
import { tail, flatten } from 'vs/base/common/arrays';
import URI from 'vs/base/common/uri';
import { IReference, Disposable } from 'vs/base/common/lifecycle';
import { Event, Emitter } from 'vs/base/common/event';
import { Registry } from 'vs/platform/registry/common/platform';
import { visit, JSONVisitor } from 'vs/base/common/json';
import { ITextModel, IIdentifiedSingleEditOperation } from 'vs/editor/common/model';
import { EditorModel } from 'vs/workbench/common/editor';
import { IConfigurationNode, IConfigurationRegistry, Extensions, OVERRIDE_PROPERTY_PATTERN, IConfigurationPropertySchema, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry';
import { ISettingsEditorModel, IKeybindingsEditorModel, ISettingsGroup, ISetting, IFilterResult, IGroupFilter, ISettingMatcher, ISettingMatch, ISearchResultGroup, IFilterMetadata } from 'vs/workbench/services/preferences/common/preferences';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { ITextEditorModel } from 'vs/editor/common/services/resolverService';
import { IRange, Range } from 'vs/editor/common/core/range';
import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
import { Selection } from 'vs/editor/common/core/selection';
import { flatten, tail } from 'vs/base/common/arrays';
import { IStringDictionary } from 'vs/base/common/collections';
import { Emitter, Event } from 'vs/base/common/event';
import { JSONVisitor, visit } from 'vs/base/common/json';
import { Disposable, IReference } from 'vs/base/common/lifecycle';
import * as map from 'vs/base/common/map';
import { assign } from 'vs/base/common/objects';
import URI from 'vs/base/common/uri';
import { IRange, Range } from 'vs/editor/common/core/range';
import { Selection } from 'vs/editor/common/core/selection';
import { IIdentifiedSingleEditOperation, ITextModel } from 'vs/editor/common/model';
import { ITextEditorModel } from 'vs/editor/common/services/resolverService';
import * as nls from 'vs/nls';
import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
import { ConfigurationScope, Extensions, IConfigurationNode, IConfigurationPropertySchema, IConfigurationRegistry, OVERRIDE_PROPERTY_PATTERN } from 'vs/platform/configuration/common/configurationRegistry';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { Registry } from 'vs/platform/registry/common/platform';
import { EditorModel } from 'vs/workbench/common/editor';
import { IFilterMetadata, IFilterResult, IGroupFilter, IKeybindingsEditorModel, ISearchResultGroup, ISetting, ISettingMatch, ISettingMatcher, ISettingsEditorModel, ISettingsGroup } from 'vs/workbench/services/preferences/common/preferences';
export abstract class AbstractSettingsModel extends EditorModel {
@@ -189,7 +189,8 @@ export class SettingsEditorModel extends AbstractSettingsModel implements ISetti
settings: filteredSettings
}],
title: modelGroup.title,
titleRange: modelGroup.titleRange
titleRange: modelGroup.titleRange,
contributedByExtension: !!modelGroup.contributedByExtension
};
}
@@ -470,7 +471,10 @@ export class DefaultSettings extends Disposable {
value: setting.value,
range: null,
valueRange: null,
overrides: []
overrides: [],
type: setting.type,
enum: setting.enum,
enumDescriptions: setting.enumDescriptions
};
}
return null;
@@ -489,7 +493,8 @@ export class DefaultSettings extends Disposable {
};
}
private parseConfig(config: IConfigurationNode, result: ISettingsGroup[], configurations: IConfigurationNode[], settingsGroup?: ISettingsGroup): ISettingsGroup[] {
private parseConfig(config: IConfigurationNode, result: ISettingsGroup[], configurations: IConfigurationNode[], settingsGroup?: ISettingsGroup, seenSettings?: { [key: string]: boolean }): ISettingsGroup[] {
seenSettings = seenSettings ? seenSettings : {};
let title = config.title;
if (!title) {
const configWithTitleAndSameId = configurations.filter(c => c.id === config.id && c.title)[0];
@@ -501,7 +506,7 @@ export class DefaultSettings extends Disposable {
if (!settingsGroup) {
settingsGroup = result.filter(g => g.title === title)[0];
if (!settingsGroup) {
settingsGroup = { sections: [{ settings: [] }], id: config.id, title: title, titleRange: null, range: null };
settingsGroup = { sections: [{ settings: [] }], id: config.id, title: title, titleRange: null, range: null, contributedByExtension: !!config.contributedByExtension };
result.push(settingsGroup);
}
} else {
@@ -510,17 +515,23 @@ export class DefaultSettings extends Disposable {
}
if (config.properties) {
if (!settingsGroup) {
settingsGroup = { sections: [{ settings: [] }], id: config.id, title: config.id, titleRange: null, range: null };
settingsGroup = { sections: [{ settings: [] }], id: config.id, title: config.id, titleRange: null, range: null, contributedByExtension: !!config.contributedByExtension };
result.push(settingsGroup);
}
const configurationSettings: ISetting[] = [...settingsGroup.sections[settingsGroup.sections.length - 1].settings, ...this.parseSettings(config.properties)];
const configurationSettings: ISetting[] = [];
for (const setting of [...settingsGroup.sections[settingsGroup.sections.length - 1].settings, ...this.parseSettings(config.properties)]) {
if (!seenSettings[setting.key]) {
configurationSettings.push(setting);
seenSettings[setting.key] = true;
}
}
if (configurationSettings.length) {
configurationSettings.sort((a, b) => a.key.localeCompare(b.key));
settingsGroup.sections[settingsGroup.sections.length - 1].settings = configurationSettings;
}
}
if (config.allOf) {
config.allOf.forEach(c => this.parseConfig(c, result, configurations, settingsGroup));
config.allOf.forEach(c => this.parseConfig(c, result, configurations, settingsGroup, seenSettings));
}
return result;
}
@@ -537,14 +548,27 @@ export class DefaultSettings extends Disposable {
}
private parseSettings(settingsObject: { [path: string]: IConfigurationPropertySchema; }): ISetting[] {
let result = [];
let result: ISetting[] = [];
for (let key in settingsObject) {
const prop = settingsObject[key];
if (!prop.deprecationMessage && this.matchesScope(prop)) {
const value = prop.default;
const description = (prop.description || '').split('\n');
const overrides = OVERRIDE_PROPERTY_PATTERN.test(key) ? this.parseOverrideSettings(prop.default) : [];
result.push({ key, value, description, range: null, keyRange: null, valueRange: null, descriptionRanges: [], overrides });
result.push({
key,
value,
description,
range: null,
keyRange: null,
valueRange: null,
descriptionRanges: [],
overrides,
type: prop.type,
enum: prop.enum,
enumDescriptions: prop.enumDescriptions,
tags: prop.tags
});
}
}
return result;
@@ -730,11 +754,15 @@ export class DefaultSettingsEditorModel extends AbstractSettingsModel implements
private copySetting(setting: ISetting): ISetting {
return <ISetting>{
description: setting.description,
type: setting.type,
enum: setting.enum,
enumDescriptions: setting.enumDescriptions,
key: setting.key,
value: setting.value,
range: setting.range,
overrides: [],
overrideOf: setting.overrideOf
overrideOf: setting.overrideOf,
tags: setting.tags
};
}
@@ -838,12 +866,8 @@ class SettingsContentBuilder {
private pushSetting(setting: ISetting, indent: string): void {
const settingStart = this.lineCountWithOffset + 1;
setting.descriptionRanges = [];
const descriptionPreValue = indent + '// ';
for (const line of setting.description) {
this._contentByLines.push(descriptionPreValue + line);
setting.descriptionRanges.push({ startLineNumber: this.lineCountWithOffset, startColumn: this.lastLine.indexOf(line) + 1, endLineNumber: this.lineCountWithOffset, endColumn: this.lastLine.length });
}
this.pushSettingDescription(setting, indent);
let preValueConent = indent;
const keyString = JSON.stringify(setting.key);
@@ -860,6 +884,33 @@ class SettingsContentBuilder {
setting.range = { startLineNumber: settingStart, startColumn: 1, endLineNumber: this.lineCountWithOffset, endColumn: this.lastLine.length };
}
private pushSettingDescription(setting: ISetting, indent: string): void {
const fixSettingLink = line => line.replace(/`#(.*)#`/g, (match, settingName) => `\`${settingName}\``);
setting.descriptionRanges = [];
const descriptionPreValue = indent + '// ';
for (let line of setting.description) {
// Remove setting link tag
line = fixSettingLink(line);
this._contentByLines.push(descriptionPreValue + line);
setting.descriptionRanges.push({ startLineNumber: this.lineCountWithOffset, startColumn: this.lastLine.indexOf(line) + 1, endLineNumber: this.lineCountWithOffset, endColumn: this.lastLine.length });
}
if (setting.enumDescriptions && setting.enumDescriptions.some(desc => !!desc)) {
setting.enumDescriptions.forEach((desc, i) => {
const displayEnum = escapeInvisibleChars(setting.enum[i]);
const line = desc ?
`${displayEnum}: ${fixSettingLink(desc)}` :
displayEnum;
this._contentByLines.push(` // - ${line}`);
setting.descriptionRanges.push({ startLineNumber: this.lineCountWithOffset, startColumn: this.lastLine.indexOf(line) + 1, endLineNumber: this.lineCountWithOffset, endColumn: this.lastLine.length });
});
}
}
private pushValue(setting: ISetting, preValueConent: string, indent: string): void {
let valueString = JSON.stringify(setting.value, null, indent);
if (valueString && (typeof setting.value === 'object')) {
@@ -892,6 +943,12 @@ class SettingsContentBuilder {
}
}
function escapeInvisibleChars(enumValue: string): string {
return enumValue && enumValue
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r');
}
export function defaultKeybindingsContents(keybindingService: IKeybindingService): string {
const defaultsHeader = '// ' + nls.localize('defaultKeybindingsHeader', "Overwrite key bindings by placing them into your key bindings file.");
return defaultsHeader + '\n' + keybindingService.getDefaultKeybindingsContent();

View File

@@ -3,7 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as lifecycle from 'vs/base/common/lifecycle';
import { Disposable } from 'vs/base/common/lifecycle';
import { TPromise } from 'vs/base/common/winjs.base';
import * as types from 'vs/base/common/types';
import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar';
@@ -21,21 +21,20 @@ interface ProgressState {
whileDelay?: number;
}
export abstract class ScopedService {
protected toDispose: lifecycle.IDisposable[];
export abstract class ScopedService extends Disposable {
constructor(private viewletService: IViewletService, private panelService: IPanelService, private scopeId: string) {
this.toDispose = [];
super();
this.registerListeners();
}
public registerListeners(): void {
this.toDispose.push(this.viewletService.onDidViewletOpen(viewlet => this.onScopeOpened(viewlet.getId())));
this.toDispose.push(this.panelService.onDidPanelOpen(panel => this.onScopeOpened(panel.getId())));
registerListeners(): void {
this._register(this.viewletService.onDidViewletOpen(viewlet => this.onScopeOpened(viewlet.getId())));
this._register(this.panelService.onDidPanelOpen(panel => this.onScopeOpened(panel.getId())));
this.toDispose.push(this.viewletService.onDidViewletClose(viewlet => this.onScopeClosed(viewlet.getId())));
this.toDispose.push(this.panelService.onDidPanelClose(panel => this.onScopeClosed(panel.getId())));
this._register(this.viewletService.onDidViewletClose(viewlet => this.onScopeClosed(viewlet.getId())));
this._register(this.panelService.onDidPanelClose(panel => this.onScopeClosed(panel.getId())));
}
private onScopeClosed(scopeId: string) {
@@ -50,13 +49,13 @@ export abstract class ScopedService {
}
}
public abstract onScopeActivated(): void;
abstract onScopeActivated(): void;
public abstract onScopeDeactivated(): void;
abstract onScopeDeactivated(): void;
}
export class WorkbenchProgressService extends ScopedService implements IProgressService {
public _serviceBrand: any;
export class ScopedProgressService extends ScopedService implements IProgressService {
_serviceBrand: any;
private isActive: boolean;
private progressbar: ProgressBar;
private progressState: ProgressState;
@@ -75,11 +74,11 @@ export class WorkbenchProgressService extends ScopedService implements IProgress
this.progressState = Object.create(null);
}
public onScopeDeactivated(): void {
onScopeDeactivated(): void {
this.isActive = false;
}
public onScopeActivated(): void {
onScopeActivated(): void {
this.isActive = true;
// Return early if progress state indicates that progress is done
@@ -127,14 +126,14 @@ export class WorkbenchProgressService extends ScopedService implements IProgress
this.progressState.whileDelay = void 0;
}
public show(infinite: boolean, delay?: number): IProgressRunner;
public show(total: number, delay?: number): IProgressRunner;
public show(infiniteOrTotal: any, delay?: number): IProgressRunner {
show(infinite: boolean, delay?: number): IProgressRunner;
show(total: number, delay?: number): IProgressRunner;
show(infiniteOrTotal: boolean | number, delay?: number): IProgressRunner {
let infinite: boolean;
let total: number;
// Sort out Arguments
if (infiniteOrTotal === false || infiniteOrTotal === true) {
if (typeof infiniteOrTotal === 'boolean') {
infinite = infiniteOrTotal;
} else {
total = infiniteOrTotal;
@@ -207,7 +206,7 @@ export class WorkbenchProgressService extends ScopedService implements IProgress
};
}
public showWhile(promise: TPromise<any>, delay?: number): TPromise<void> {
showWhile(promise: TPromise<any>, delay?: number): TPromise<void> {
let stack: boolean = !!this.progressState.whilePromise;
// Reset State
@@ -252,8 +251,49 @@ export class WorkbenchProgressService extends ScopedService implements IProgress
this.progressbar.infinite().show(delay);
}
}
}
public dispose(): void {
this.toDispose = lifecycle.dispose(this.toDispose);
export class ProgressService implements IProgressService {
_serviceBrand: any;
constructor(private progressbar: ProgressBar) { }
show(infinite: boolean, delay?: number): IProgressRunner;
show(total: number, delay?: number): IProgressRunner;
show(infiniteOrTotal: boolean | number, delay?: number): IProgressRunner {
if (typeof infiniteOrTotal === 'boolean') {
this.progressbar.infinite().show(delay);
} else {
this.progressbar.total(infiniteOrTotal).show(delay);
}
return {
total: (total: number) => {
this.progressbar.total(total);
},
worked: (worked: number) => {
if (this.progressbar.hasTotal()) {
this.progressbar.worked(worked);
} else {
this.progressbar.infinite().show();
}
},
done: () => {
this.progressbar.stop().hide();
}
};
}
showWhile(promise: TPromise<any>, delay?: number): TPromise<void> {
const stop = () => {
this.progressbar.stop().hide();
};
this.progressbar.infinite().show(delay);
return promise.then(stop, stop);
}
}

View File

@@ -8,17 +8,19 @@ import 'vs/css!./media/progressService2';
import * as dom from 'vs/base/browser/dom';
import { localize } from 'vs/nls';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { IProgressService2, IProgressOptions, ProgressLocation, IProgress, IProgressStep, Progress, emptyProgress } from 'vs/platform/progress/common/progress';
import { IProgressService2, IProgressOptions, IProgressStep, ProgressLocation } from 'vs/workbench/services/progress/common/progress';
import { IProgress, emptyProgress, Progress } from 'vs/platform/progress/common/progress';
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
import { OcticonLabel } from 'vs/base/browser/ui/octiconLabel/octiconLabel';
import { Registry } from 'vs/platform/registry/common/platform';
import { StatusbarAlignment, IStatusbarRegistry, StatusbarItemDescriptor, Extensions, IStatusbarItem } from 'vs/workbench/browser/parts/statusbar/statusbar';
import { TPromise } from 'vs/base/common/winjs.base';
import { always } from 'vs/base/common/async';
import { always, timeout } from 'vs/base/common/async';
import { ProgressBadge, IActivityService } from 'vs/workbench/services/activity/common/activity';
import { INotificationService, Severity, INotificationHandle, INotificationActions } from 'vs/platform/notification/common/notification';
import { Action } from 'vs/base/common/actions';
import { once } from 'vs/base/common/event';
import { ViewContainer } from 'vs/workbench/common/views';
class WindowProgressItem implements IStatusbarItem {
@@ -90,6 +92,15 @@ export class ProgressService2 implements IProgressService2 {
withProgress<P extends Thenable<R>, R=any>(options: IProgressOptions, task: (progress: IProgress<IProgressStep>) => P, onDidCancel?: () => void): P {
const { location } = options;
if (location instanceof ViewContainer) {
const viewlet = this._viewletService.getViewlet(location.id);
if (viewlet) {
return this._withViewletProgress(location.id, task);
}
console.warn(`Bad progress location: ${location.id}`);
return undefined;
}
switch (location) {
case ProgressLocation.Notification:
return this._withNotificationProgress(options, task, onDidCancel);
@@ -119,8 +130,8 @@ export class ProgressService2 implements IProgressService2 {
this._updateWindowProgress();
// show progress for at least 150ms
always(TPromise.join([
TPromise.timeout(150),
always(Promise.all([
timeout(150),
promise
]), () => {
const idx = this._stack.indexOf(task);
@@ -131,7 +142,7 @@ export class ProgressService2 implements IProgressService2 {
}, 150);
// cancel delay if promise finishes below 150ms
always(TPromise.wrap(promise), () => clearTimeout(delayHandle));
always(promise, () => clearTimeout(delayHandle));
return promise;
}
@@ -141,25 +152,32 @@ export class ProgressService2 implements IProgressService2 {
} else {
const [options, progress] = this._stack[idx];
let text = options.title;
if (progress.value && progress.value.message) {
text = progress.value.message;
}
let progressTitle = options.title;
let progressMessage = progress.value && progress.value.message;
let text: string;
let title: string;
if (!text) {
// no message -> no progress. try with next on stack
if (progressTitle && progressMessage) {
// <title>: <message>
text = localize('progress.text2', "{0}: {1}", progressTitle, progressMessage);
title = options.source ? localize('progress.title3', "[{0}] {1}: {2}", options.source, progressTitle, progressMessage) : text;
} else if (progressTitle) {
// <title>
text = progressTitle;
title = options.source ? localize('progress.title2', "[{0}]: {1}", options.source, progressTitle) : text;
} else if (progressMessage) {
// <message>
text = progressMessage;
title = options.source ? localize('progress.title2', "[{0}]: {1}", options.source, progressMessage) : text;
} else {
// no title, no message -> no progress. try with next on stack
this._updateWindowProgress(idx + 1);
return;
}
let title = text;
if (options.title && options.title !== title) {
title = localize('progress.subtitle', "{0} - {1}", options.title, title);
}
if (options.source) {
title = localize('progress.title', "{0}: {1}", options.source, title);
}
WindowProgressItem.Instance.text = text;
WindowProgressItem.Instance.title = title;
WindowProgressItem.Instance.show();
@@ -196,7 +214,7 @@ export class ProgressService2 implements IProgressService2 {
const handle = this._notificationService.notify({
severity: Severity.Info,
message: options.title,
message,
source: options.source,
actions
});
@@ -225,7 +243,14 @@ export class ProgressService2 implements IProgressService2 {
handle = createNotification(message, increment);
} else {
if (typeof message === 'string') {
handle.updateMessage(message);
let newMessage: string;
if (typeof options.title === 'string') {
newMessage = `${options.title}: ${message}`; // always prefix with overall title if we have it (https://github.com/Microsoft/vscode/issues/50932)
} else {
newMessage = message;
}
handle.updateMessage(newMessage);
}
if (typeof increment === 'number') {
@@ -245,7 +270,7 @@ export class ProgressService2 implements IProgressService2 {
});
// Show progress for at least 800ms and then hide once done or canceled
always(TPromise.join([TPromise.timeout(800), p]), () => {
always(Promise.all([timeout(800), p]), () => {
if (handle) {
handle.close();
}

View File

@@ -0,0 +1,39 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IProgress } from 'vs/platform/progress/common/progress';
import { ViewContainer } from 'vs/workbench/common/views';
export enum ProgressLocation {
Explorer = 1,
Scm = 3,
Extensions = 5,
Window = 10,
Notification = 15
}
export interface IProgressOptions {
location: ProgressLocation | ViewContainer;
title?: string;
source?: string;
total?: number;
cancellable?: boolean;
}
export interface IProgressStep {
message?: string;
increment?: number;
}
export const IProgressService2 = createDecorator<IProgressService2>('progressService2');
export interface IProgressService2 {
_serviceBrand: any;
withProgress<P extends Thenable<R>, R=any>(options: IProgressOptions, task: (progress: IProgress<IProgressStep>) => P, onDidCancel?: () => void): P;
}

View File

@@ -8,10 +8,10 @@
import * as assert from 'assert';
import { IAction, IActionItem } from 'vs/base/common/actions';
import { Promise, TPromise } from 'vs/base/common/winjs.base';
import { IEditorControl } from 'vs/platform/editor/common/editor';
import { IEditorControl } from 'vs/workbench/common/editor';
import { Viewlet, ViewletDescriptor } from 'vs/workbench/browser/viewlet';
import { IPanel } from 'vs/workbench/common/panel';
import { WorkbenchProgressService, ScopedService } from 'vs/workbench/services/progress/browser/progressService';
import { ScopedProgressService, ScopedService } from 'vs/workbench/services/progress/browser/progressService';
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
import { IPanelService } from 'vs/workbench/services/panel/common/panelService';
import { IViewlet } from 'vs/workbench/common/viewlet';
@@ -244,7 +244,7 @@ suite('Progress Service', () => {
let testProgressBar = new TestProgressBar();
let viewletService = new TestViewletService();
let panelService = new TestPanelService();
let service = new WorkbenchProgressService((<any>testProgressBar), 'test.scopeId', true, viewletService, panelService);
let service = new ScopedProgressService((<any>testProgressBar), 'test.scopeId', true, viewletService, panelService);
// Active: Show (Infinite)
let fn = service.show(true);

View File

@@ -18,13 +18,14 @@ import * as objects from 'vs/base/common/objects';
import * as arrays from 'vs/base/common/arrays';
import * as platform from 'vs/base/common/platform';
import * as strings from 'vs/base/common/strings';
import * as normalization from 'vs/base/common/normalization';
import * as types from 'vs/base/common/types';
import * as glob from 'vs/base/common/glob';
import { IProgress, IUncachedSearchStats } from 'vs/platform/search/common/search';
import * as extfs from 'vs/base/node/extfs';
import * as flow from 'vs/base/node/flow';
import { IRawFileMatch, ISerializedSearchComplete, IRawSearch, ISearchEngine, IFolderSearch } from './search';
import { IRawFileMatch, IRawSearch, ISearchEngine, IFolderSearch, ISerializedSearchSuccess } from './search';
import { spawnRipgrepCmd } from './ripgrepFileSearch';
import { rgErrorMsgForDisplay } from './ripgrepTextSearch';
@@ -122,78 +123,61 @@ export class FileWalker {
this.fileWalkStartTime = Date.now();
// Support that the file pattern is a full path to a file that exists
this.checkFilePatternAbsoluteMatch((exists, size) => {
if (this.isCanceled) {
return done(null, this.isLimitHit);
}
if (this.isCanceled) {
return done(null, this.isLimitHit);
}
// Report result from file pattern if matching
if (exists) {
this.resultCount++;
onResult({
relativePath: this.filePattern,
basename: path.basename(this.filePattern),
size
});
// Optimization: a match on an absolute path is a good result and we do not
// continue walking the entire root paths array for other matches because
// it is very unlikely that another file would match on the full absolute path
return done(null, this.isLimitHit);
}
// For each extra file
if (extraFiles) {
extraFiles.forEach(extraFilePath => {
const basename = path.basename(extraFilePath);
if (this.globalExcludePattern && this.globalExcludePattern(extraFilePath, basename)) {
return; // excluded
}
// File: Check for match on file pattern and include pattern
this.matchFile(onResult, { relativePath: extraFilePath /* no workspace relative path */, basename });
});
}
let traverse = this.nodeJSTraversal;
if (!this.maxFilesize) {
if (this.useRipgrep) {
this.traversal = Traversal.Ripgrep;
traverse = this.cmdTraversal;
} else if (platform.isMacintosh) {
this.traversal = Traversal.MacFind;
traverse = this.cmdTraversal;
// Disable 'dir' for now (#11181, #11179, #11183, #11182).
} /* else if (platform.isWindows) {
this.traversal = Traversal.WindowsDir;
traverse = this.windowsDirTraversal;
} */ else if (platform.isLinux) {
this.traversal = Traversal.LinuxFind;
traverse = this.cmdTraversal;
// For each extra file
if (extraFiles) {
extraFiles.forEach(extraFilePath => {
const basename = path.basename(extraFilePath);
if (this.globalExcludePattern && this.globalExcludePattern(extraFilePath, basename)) {
return; // excluded
}
}
const isNodeTraversal = traverse === this.nodeJSTraversal;
if (!isNodeTraversal) {
this.cmdForkStartTime = Date.now();
}
// For each root folder
flow.parallel<IFolderSearch, void>(folderQueries, (folderQuery: IFolderSearch, rootFolderDone: (err: Error, result: void) => void) => {
this.call(traverse, this, folderQuery, onResult, onMessage, (err?: Error) => {
if (err) {
const errorMessage = toErrorMessage(err);
console.error(errorMessage);
this.errors.push(errorMessage);
rootFolderDone(err, undefined);
} else {
rootFolderDone(undefined, undefined);
}
});
}, (errors, result) => {
const err = errors ? errors.filter(e => !!e)[0] : null;
done(err, this.isLimitHit);
// File: Check for match on file pattern and include pattern
this.matchFile(onResult, { relativePath: extraFilePath /* no workspace relative path */, basename });
});
}
let traverse = this.nodeJSTraversal;
if (!this.maxFilesize) {
if (this.useRipgrep) {
this.traversal = Traversal.Ripgrep;
traverse = this.cmdTraversal;
} else if (platform.isMacintosh) {
this.traversal = Traversal.MacFind;
traverse = this.cmdTraversal;
// Disable 'dir' for now (#11181, #11179, #11183, #11182).
} /* else if (platform.isWindows) {
this.traversal = Traversal.WindowsDir;
traverse = this.windowsDirTraversal;
} */ else if (platform.isLinux) {
this.traversal = Traversal.LinuxFind;
traverse = this.cmdTraversal;
}
}
const isNodeTraversal = traverse === this.nodeJSTraversal;
if (!isNodeTraversal) {
this.cmdForkStartTime = Date.now();
}
// For each root folder
flow.parallel<IFolderSearch, void>(folderQueries, (folderQuery: IFolderSearch, rootFolderDone: (err: Error, result: void) => void) => {
this.call(traverse, this, folderQuery, onResult, onMessage, (err?: Error) => {
if (err) {
const errorMessage = toErrorMessage(err);
console.error(errorMessage);
this.errors.push(errorMessage);
rootFolderDone(err, undefined);
} else {
rootFolderDone(undefined, undefined);
}
});
}, (errors, result) => {
const err = errors ? errors.filter(e => !!e)[0] : null;
done(err, this.isLimitHit);
});
}
@@ -222,7 +206,6 @@ export class FileWalker {
const useRipgrep = this.useRipgrep;
let noSiblingsClauses: boolean;
let filePatternSeen = false;
if (useRipgrep) {
const ripgrep = spawnRipgrepCmd(this.config, folderQuery, this.config.includePattern, this.folderExcludePatterns.get(folderQuery.folder).expression);
cmd = ripgrep.cmd;
@@ -244,7 +227,7 @@ export class FileWalker {
}
process.on('exit', killCmd);
this.collectStdout(cmd, 'utf8', useRipgrep, (err: Error, stdout?: string, last?: boolean) => {
this.collectStdout(cmd, 'utf8', useRipgrep, onMessage, (err: Error, stdout?: string, last?: boolean) => {
if (err) {
done(err);
return;
@@ -255,7 +238,7 @@ export class FileWalker {
}
// Mac: uses NFD unicode form on disk, but we want NFC
const normalized = leftover + (isMac ? strings.normalizeNFC(stdout) : stdout);
const normalized = leftover + (isMac ? normalization.normalizeNFC(stdout) : stdout);
const relativeFiles = normalized.split(useRipgrep ? '\n' : '\n./');
if (!useRipgrep && first && normalized.length >= 2) {
first = false;
@@ -281,9 +264,6 @@ export class FileWalker {
if (useRipgrep && noSiblingsClauses) {
for (const relativePath of relativeFiles) {
if (relativePath === this.filePattern) {
filePatternSeen = true;
}
const basename = path.basename(relativePath);
this.matchFile(onResult, { base: rootFolder, relativePath, basename });
if (this.isLimitHit) {
@@ -292,22 +272,9 @@ export class FileWalker {
}
}
if (last || this.isLimitHit) {
if (!filePatternSeen) {
this.checkFilePatternRelativeMatch(folderQuery.folder, (match, size) => {
if (match) {
this.resultCount++;
onResult({
base: folderQuery.folder,
relativePath: this.filePattern,
basename: path.basename(this.filePattern),
});
}
done();
});
} else {
done();
}
done();
}
return;
}
@@ -378,7 +345,7 @@ export class FileWalker {
*/
public readStdout(cmd: childProcess.ChildProcess, encoding: string, isRipgrep: boolean, cb: (err: Error, stdout?: string) => void): void {
let all = '';
this.collectStdout(cmd, encoding, isRipgrep, (err: Error, stdout?: string, last?: boolean) => {
this.collectStdout(cmd, encoding, isRipgrep, () => { }, (err: Error, stdout?: string, last?: boolean) => {
if (err) {
cb(err);
return;
@@ -391,35 +358,46 @@ export class FileWalker {
});
}
private collectStdout(cmd: childProcess.ChildProcess, encoding: string, isRipgrep: boolean, cb: (err: Error, stdout?: string, last?: boolean) => void): void {
let done = (err: Error, stdout?: string, last?: boolean) => {
private collectStdout(cmd: childProcess.ChildProcess, encoding: string, isRipgrep: boolean, onMessage: (message: IProgress) => void, cb: (err: Error, stdout?: string, last?: boolean) => void): void {
let onData = (err: Error, stdout?: string, last?: boolean) => {
if (err || last) {
done = () => { };
onData = () => { };
this.cmdForkResultTime = Date.now();
}
cb(err, stdout, last);
};
this.forwardData(cmd.stdout, encoding, done);
const stderr = this.collectData(cmd.stderr);
let gotData = false;
cmd.stdout.once('data', () => gotData = true);
if (cmd.stdout) {
// Should be non-null, but #38195
this.forwardData(cmd.stdout, encoding, onData);
cmd.stdout.once('data', () => gotData = true);
} else {
onMessage({ message: 'stdout is null' });
}
let stderr: Buffer[];
if (cmd.stderr) {
// Should be non-null, but #38195
stderr = this.collectData(cmd.stderr);
} else {
onMessage({ message: 'stderr is null' });
}
cmd.on('error', (err: Error) => {
done(err);
onData(err);
});
cmd.on('close', (code: number) => {
// ripgrep returns code=1 when no results are found
let stderrText, displayMsg: string;
if (isRipgrep ? (!gotData && (stderrText = this.decodeData(stderr, encoding)) && (displayMsg = rgErrorMsgForDisplay(stderrText))) : code !== 0) {
done(new Error(`command failed with error code ${code}: ${this.decodeData(stderr, encoding)}`));
onData(new Error(`command failed with error code ${code}: ${this.decodeData(stderr, encoding)}`));
} else {
if (isRipgrep && this.exists && code === 0) {
this.isLimitHit = true;
}
done(null, '', true);
onData(null, '', true);
}
});
}
@@ -484,6 +462,7 @@ export class FileWalker {
const filePattern = this.filePattern;
function matchDirectory(entries: IDirectoryEntry[]) {
self.directoriesWalked++;
const hasSibling = glob.hasSiblingFn(() => entries.map(entry => entry.basename));
for (let i = 0, n = entries.length; i < n; i++) {
const entry = entries[i];
const { relativePath, basename } = entry;
@@ -492,7 +471,7 @@ export class FileWalker {
// If the user searches for the exact file name, we adjust the glob matching
// to ignore filtering by siblings because the user seems to know what she
// is searching for and we want to include the result in that case anyway
if (excludePattern.test(relativePath, basename, () => filePattern !== basename ? entries.map(entry => entry.basename) : [])) {
if (excludePattern.test(relativePath, basename, filePattern !== basename ? hasSibling : undefined)) {
continue;
}
@@ -523,25 +502,11 @@ export class FileWalker {
return done();
}
// Support relative paths to files from a root resource (ignores excludes)
return this.checkFilePatternRelativeMatch(folderQuery.folder, (match, size) => {
if (this.isCanceled || this.isLimitHit) {
return done();
}
if (this.isCanceled || this.isLimitHit) {
return done();
}
// Report result from file pattern if matching
if (match) {
this.resultCount++;
onResult({
base: folderQuery.folder,
relativePath: this.filePattern,
basename: path.basename(this.filePattern),
size
});
}
return this.doWalk(folderQuery, '', files, onResult, done);
});
return this.doWalk(folderQuery, '', files, onResult, done);
});
}
@@ -561,32 +526,11 @@ export class FileWalker {
};
}
private checkFilePatternAbsoluteMatch(clb: (exists: boolean, size?: number) => void): void {
if (!this.filePattern || !path.isAbsolute(this.filePattern)) {
return clb(false);
}
return fs.stat(this.filePattern, (error, stat) => {
return clb(!error && !stat.isDirectory(), stat && stat.size); // only existing files
});
}
private checkFilePatternRelativeMatch(basePath: string, clb: (matchPath: string, size?: number) => void): void {
if (!this.filePattern || path.isAbsolute(this.filePattern)) {
return clb(null);
}
const absolutePath = path.join(basePath, this.filePattern);
return fs.stat(absolutePath, (error, stat) => {
return clb(!error && !stat.isDirectory() ? absolutePath : null, stat && stat.size); // only existing files
});
}
private doWalk(folderQuery: IFolderSearch, relativeParentPath: string, files: string[], onResult: (result: IRawFileMatch) => void, done: (error: Error) => void): void {
const rootFolder = folderQuery.folder;
// Execute tasks on each file in parallel to optimize throughput
const hasSibling = glob.hasSiblingFn(() => files);
flow.parallel(files, (file: string, clb: (error: Error, result: {}) => void): void => {
// Check canceled
@@ -594,17 +538,12 @@ export class FileWalker {
return clb(null, undefined);
}
// Check exclude pattern
// If the user searches for the exact file name, we adjust the glob matching
// to ignore filtering by siblings because the user seems to know what she
// is searching for and we want to include the result in that case anyway
let siblings = files;
if (this.config.filePattern === file) {
siblings = [];
}
// Check exclude pattern
let currentRelativePath = relativeParentPath ? [relativeParentPath, file].join(path.sep) : file;
if (this.folderExcludePatterns.get(folderQuery.folder).test(currentRelativePath, file, () => siblings)) {
if (this.folderExcludePatterns.get(folderQuery.folder).test(currentRelativePath, file, this.config.filePattern !== file ? hasSibling : undefined)) {
return clb(null, undefined);
}
@@ -741,9 +680,10 @@ export class Engine implements ISearchEngine<IRawFileMatch> {
this.walker = new FileWalker(config);
}
public search(onResult: (result: IRawFileMatch) => void, onProgress: (progress: IProgress) => void, done: (error: Error, complete: ISerializedSearchComplete) => void): void {
public search(onResult: (result: IRawFileMatch) => void, onProgress: (progress: IProgress) => void, done: (error: Error, complete: ISerializedSearchSuccess) => void): void {
this.walker.walk(this.folderQueries, this.extraFiles, onResult, onProgress, (err: Error, isLimitHit: boolean) => {
done(err, {
type: 'success',
limitHit: isLimitHit,
stats: this.walker.getStats()
});
@@ -790,9 +730,9 @@ class AbsoluteAndRelativeParsedExpression {
this.relativeParsedExpr = relativeGlobExpr && glob.parse(relativeGlobExpr, { trimForExclusions: true });
}
public test(_path: string, basename?: string, siblingsFn?: () => string[] | TPromise<string[]>): string | TPromise<string> {
return (this.relativeParsedExpr && this.relativeParsedExpr(_path, basename, siblingsFn)) ||
(this.absoluteParsedExpr && this.absoluteParsedExpr(path.join(this.root, _path), basename, siblingsFn));
public test(_path: string, basename?: string, hasSibling?: (name: string) => boolean | TPromise<boolean>): string | TPromise<string> {
return (this.relativeParsedExpr && this.relativeParsedExpr(_path, basename, hasSibling)) ||
(this.absoluteParsedExpr && this.absoluteParsedExpr(path.join(this.root, _path), basename, hasSibling));
}
public getBasenameTerms(): string[] {
@@ -820,4 +760,4 @@ class AbsoluteAndRelativeParsedExpression {
return pathTerms;
}
}
}

View File

@@ -6,23 +6,26 @@
'use strict';
import * as fs from 'fs';
import { isAbsolute, sep, join } from 'path';
import * as gracefulFs from 'graceful-fs';
gracefulFs.gracefulify(fs);
import { join, sep } from 'path';
import * as arrays from 'vs/base/common/arrays';
import * as objects from 'vs/base/common/objects';
import * as strings from 'vs/base/common/strings';
import { PPromise, TPromise } from 'vs/base/common/winjs.base';
import { FileWalker, Engine as FileSearchEngine } from 'vs/workbench/services/search/node/fileSearch';
import { TPromise } from 'vs/base/common/winjs.base';
import { compareItemsByScore, IItemAccessor, prepareQuery, ScorerCache } from 'vs/base/parts/quickopen/common/quickOpenScorer';
import { MAX_FILE_SIZE } from 'vs/platform/files/node/files';
import { ICachedSearchStats, IProgress } from 'vs/platform/search/common/search';
import { Engine as FileSearchEngine, FileWalker } from 'vs/workbench/services/search/node/fileSearch';
import { RipgrepEngine } from 'vs/workbench/services/search/node/ripgrepTextSearch';
import { Engine as TextSearchEngine } from 'vs/workbench/services/search/node/textSearch';
import { TextSearchWorkerProvider } from 'vs/workbench/services/search/node/textSearchWorkerProvider';
import { IRawSearchService, IRawSearch, IRawFileMatch, ISerializedFileMatch, ISerializedSearchProgressItem, ISerializedSearchComplete, ISearchEngine, IFileSearchProgressItem, ITelemetryEvent } from './search';
import { ICachedSearchStats, IProgress } from 'vs/platform/search/common/search';
import { compareItemsByScore, IItemAccessor, ScorerCache, prepareQuery } from 'vs/base/parts/quickopen/common/quickOpenScorer';
import { IFileSearchProgressItem, IRawFileMatch, IRawSearch, IRawSearchService, ISearchEngine, ISerializedFileMatch, ISerializedSearchComplete, ISerializedSearchProgressItem, ITelemetryEvent, ISerializedSearchSuccess } from './search';
import { Event, Emitter } from 'vs/base/common/event';
gracefulFs.gracefulify(fs);
type IProgressCallback = (p: ISerializedSearchProgressItem) => void;
type IFileProgressCallback = (p: IFileSearchProgressItem) => void;
export class SearchService implements IRawSearchService {
@@ -32,29 +35,52 @@ export class SearchService implements IRawSearchService {
private textSearchWorkerProvider: TextSearchWorkerProvider;
private telemetryPipe: (event: ITelemetryEvent) => void;
private _onTelemetry = new Emitter<ITelemetryEvent>();
readonly onTelemetry: Event<ITelemetryEvent> = this._onTelemetry.event;
public fileSearch(config: IRawSearch): PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem> {
return this.doFileSearch(FileSearchEngine, config, SearchService.BATCH_SIZE);
public fileSearch(config: IRawSearch, batchSize = SearchService.BATCH_SIZE): Event<ISerializedSearchProgressItem | ISerializedSearchComplete> {
let promise: TPromise<ISerializedSearchSuccess>;
const emitter = new Emitter<ISerializedSearchProgressItem | ISerializedSearchComplete>({
onFirstListenerDidAdd: () => {
promise = this.doFileSearch(FileSearchEngine, config, p => emitter.fire(p), batchSize)
.then(c => emitter.fire(c), err => emitter.fire({ type: 'error', error: { message: err.message, stack: err.stack } }));
},
onLastListenerRemove: () => {
promise.cancel();
}
});
return emitter.event;
}
public textSearch(config: IRawSearch): PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem> {
return config.useRipgrep ?
this.ripgrepTextSearch(config) :
this.legacyTextSearch(config);
public textSearch(config: IRawSearch): Event<ISerializedSearchProgressItem | ISerializedSearchComplete> {
let promise: TPromise<ISerializedSearchSuccess>;
const emitter = new Emitter<ISerializedSearchProgressItem | ISerializedSearchComplete>({
onFirstListenerDidAdd: () => {
promise = (config.useRipgrep ? this.ripgrepTextSearch(config, p => emitter.fire(p)) : this.legacyTextSearch(config, p => emitter.fire(p)))
.then(c => emitter.fire(c), err => emitter.fire({ type: 'error', error: { message: err.message, stack: err.stack } }));
},
onLastListenerRemove: () => {
promise.cancel();
}
});
return emitter.event;
}
public ripgrepTextSearch(config: IRawSearch): PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem> {
private ripgrepTextSearch(config: IRawSearch, progressCallback: IProgressCallback): TPromise<ISerializedSearchSuccess> {
config.maxFilesize = MAX_FILE_SIZE;
let engine = new RipgrepEngine(config);
return new PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem>((c, e, p) => {
return new TPromise<ISerializedSearchSuccess>((c, e) => {
// Use BatchedCollector to get new results to the frontend every 2s at least, until 50 results have been returned
const collector = new BatchedCollector<ISerializedFileMatch>(SearchService.BATCH_SIZE, p);
const collector = new BatchedCollector<ISerializedFileMatch>(SearchService.BATCH_SIZE, progressCallback);
engine.search((match) => {
collector.addItem(match, match.numMatches);
}, (message) => {
p(message);
progressCallback(message);
}, (error, stats) => {
collector.flush();
@@ -69,7 +95,7 @@ export class SearchService implements IRawSearchService {
});
}
public legacyTextSearch(config: IRawSearch): PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem> {
private legacyTextSearch(config: IRawSearch, progressCallback: IProgressCallback): TPromise<ISerializedSearchComplete> {
if (!this.textSearchWorkerProvider) {
this.textSearchWorkerProvider = new TextSearchWorkerProvider();
}
@@ -87,75 +113,75 @@ export class SearchService implements IRawSearchService {
}),
this.textSearchWorkerProvider);
return this.doTextSearch(engine, SearchService.BATCH_SIZE);
return this.doTextSearch(engine, progressCallback, SearchService.BATCH_SIZE);
}
public doFileSearch(EngineClass: { new(config: IRawSearch): ISearchEngine<IRawFileMatch>; }, config: IRawSearch, batchSize?: number): PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem> {
doFileSearch(EngineClass: { new(config: IRawSearch): ISearchEngine<IRawFileMatch>; }, config: IRawSearch, progressCallback: IProgressCallback, batchSize?: number): TPromise<ISerializedSearchSuccess> {
const fileProgressCallback: IFileProgressCallback = progress => {
if (Array.isArray(progress)) {
progressCallback(progress.map(m => this.rawMatchToSearchItem(m)));
} else if ((<IRawFileMatch>progress).relativePath) {
progressCallback(this.rawMatchToSearchItem(<IRawFileMatch>progress));
} else {
progressCallback(<IProgress>progress);
}
};
if (config.sortByScore) {
let sortedSearch = this.trySortedSearchFromCache(config);
let sortedSearch = this.trySortedSearchFromCache(config, fileProgressCallback);
if (!sortedSearch) {
const walkerConfig = config.maxResults ? objects.assign({}, config, { maxResults: null }) : config;
const engine = new EngineClass(walkerConfig);
sortedSearch = this.doSortedSearch(engine, config);
sortedSearch = this.doSortedSearch(engine, config, progressCallback, fileProgressCallback);
}
return new PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem>((c, e, p) => {
return new TPromise<ISerializedSearchSuccess>((c, e) => {
process.nextTick(() => { // allow caller to register progress callback first
sortedSearch.then(([result, rawMatches]) => {
const serializedMatches = rawMatches.map(rawMatch => this.rawMatchToSearchItem(rawMatch));
this.sendProgress(serializedMatches, p, batchSize);
this.sendProgress(serializedMatches, progressCallback, batchSize);
c(result);
}, e, p);
}, e);
});
}, () => {
sortedSearch.cancel();
});
}
let searchPromise: PPromise<void, IFileSearchProgressItem>;
return new PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem>((c, e, p) => {
const engine = new EngineClass(config);
searchPromise = this.doSearch(engine, batchSize)
.then(c, e, progress => {
if (Array.isArray(progress)) {
p(progress.map(m => this.rawMatchToSearchItem(m)));
} else if ((<IRawFileMatch>progress).relativePath) {
p(this.rawMatchToSearchItem(<IRawFileMatch>progress));
} else {
p(<IProgress>progress);
}
});
}, () => {
searchPromise.cancel();
});
const engine = new EngineClass(config);
return this.doSearch(engine, fileProgressCallback, batchSize);
}
private rawMatchToSearchItem(match: IRawFileMatch): ISerializedFileMatch {
return { path: match.base ? join(match.base, match.relativePath) : match.relativePath };
}
private doSortedSearch(engine: ISearchEngine<IRawFileMatch>, config: IRawSearch): PPromise<[ISerializedSearchComplete, IRawFileMatch[]], IProgress> {
let searchPromise: PPromise<void, IFileSearchProgressItem>;
let allResultsPromise = new PPromise<[ISerializedSearchComplete, IRawFileMatch[]], IFileSearchProgressItem>((c, e, p) => {
private doSortedSearch(engine: ISearchEngine<IRawFileMatch>, config: IRawSearch, progressCallback: IProgressCallback, fileProgressCallback: IFileProgressCallback): TPromise<[ISerializedSearchSuccess, IRawFileMatch[]]> {
let searchPromise: TPromise<void>;
const emitter = new Emitter<IFileSearchProgressItem>();
let allResultsPromise = new TPromise<[ISerializedSearchSuccess, IRawFileMatch[]]>((c, e) => {
let results: IRawFileMatch[] = [];
searchPromise = this.doSearch(engine, -1)
const innerProgressCallback: IFileProgressCallback = progress => {
if (Array.isArray(progress)) {
results = progress;
} else {
fileProgressCallback(progress);
emitter.fire(progress);
}
};
searchPromise = this.doSearch(engine, innerProgressCallback, -1)
.then(result => {
c([result, results]);
if (this.telemetryPipe) {
// __GDPR__TODO__ classify event
this.telemetryPipe({
eventName: 'fileSearch',
data: result.stats
});
}
}, e, progress => {
if (Array.isArray(progress)) {
results = progress;
} else {
p(progress);
}
});
// __GDPR__TODO__ classify event
this._onTelemetry.fire({
eventName: 'fileSearch',
data: result.stats
});
}, e);
}, () => {
searchPromise.cancel();
});
@@ -163,7 +189,10 @@ export class SearchService implements IRawSearchService {
let cache: Cache;
if (config.cacheKey) {
cache = this.getOrCreateCache(config.cacheKey);
cache.resultsToSearchCache[config.filePattern] = allResultsPromise;
cache.resultsToSearchCache[config.filePattern] = {
promise: allResultsPromise,
event: emitter.event
};
allResultsPromise.then(null, err => {
delete cache.resultsToSearchCache[config.filePattern];
});
@@ -171,7 +200,7 @@ export class SearchService implements IRawSearchService {
}
let chained: TPromise<void>;
return new PPromise<[ISerializedSearchComplete, IRawFileMatch[]], IProgress>((c, e, p) => {
return new TPromise<[ISerializedSearchSuccess, IRawFileMatch[]]>((c, e) => {
chained = allResultsPromise.then(([result, results]) => {
const scorerCache: ScorerCache = cache ? cache.scorerCache : Object.create(null);
const unsortedResultTime = Date.now();
@@ -180,14 +209,15 @@ export class SearchService implements IRawSearchService {
const sortedResultTime = Date.now();
c([{
type: 'success',
stats: objects.assign({}, result.stats, {
unsortedResultTime,
sortedResultTime
}),
limitHit: result.limitHit || typeof config.maxResults === 'number' && results.length > config.maxResults
}, sortedResults]);
} as ISerializedSearchSuccess, sortedResults]);
});
}, e, p);
}, e);
}, () => {
chained.cancel();
});
@@ -201,17 +231,17 @@ export class SearchService implements IRawSearchService {
return this.caches[cacheKey] = new Cache();
}
private trySortedSearchFromCache(config: IRawSearch): PPromise<[ISerializedSearchComplete, IRawFileMatch[]], IProgress> {
private trySortedSearchFromCache(config: IRawSearch, progressCallback: IFileProgressCallback): TPromise<[ISerializedSearchSuccess, IRawFileMatch[]]> {
const cache = config.cacheKey && this.caches[config.cacheKey];
if (!cache) {
return undefined;
}
const cacheLookupStartTime = Date.now();
const cached = this.getResultsFromCache(cache, config.filePattern);
const cached = this.getResultsFromCache(cache, config.filePattern, progressCallback);
if (cached) {
let chained: TPromise<void>;
return new PPromise<[ISerializedSearchComplete, IRawFileMatch[]], IProgress>((c, e, p) => {
return new TPromise<[ISerializedSearchSuccess, IRawFileMatch[]]>((c, e) => {
chained = cached.then(([result, results, cacheStats]) => {
const cacheLookupResultTime = Date.now();
return this.sortResults(config, results, cache.scorerCache)
@@ -235,13 +265,14 @@ export class SearchService implements IRawSearchService {
}
c([
{
type: 'success',
limitHit: result.limitHit || typeof config.maxResults === 'number' && results.length > config.maxResults,
stats: stats
},
} as ISerializedSearchSuccess,
sortedResults
]);
});
}, e, p);
}, e);
}, () => {
chained.cancel();
});
@@ -260,7 +291,7 @@ export class SearchService implements IRawSearchService {
return arrays.topAsync(results, compare, config.maxResults, 10000);
}
private sendProgress(results: ISerializedFileMatch[], progressCb: (batch: ISerializedFileMatch[]) => void, batchSize: number) {
private sendProgress(results: ISerializedFileMatch[], progressCb: IProgressCallback, batchSize: number) {
if (batchSize && batchSize > 0) {
for (let i = 0; i < results.length; i += batchSize) {
progressCb(results.slice(i, i + batchSize));
@@ -270,14 +301,10 @@ export class SearchService implements IRawSearchService {
}
}
private getResultsFromCache(cache: Cache, searchValue: string): PPromise<[ISerializedSearchComplete, IRawFileMatch[], CacheStats], IProgress> {
if (isAbsolute(searchValue)) {
return null; // bypass cache if user looks up an absolute path where matching goes directly on disk
}
private getResultsFromCache(cache: Cache, searchValue: string, progressCallback: IFileProgressCallback): TPromise<[ISerializedSearchSuccess, IRawFileMatch[], CacheStats]> {
// Find cache entries by prefix of search value
const hasPathSep = searchValue.indexOf(sep) >= 0;
let cached: PPromise<[ISerializedSearchComplete, IRawFileMatch[]], IFileSearchProgressItem>;
let cachedRow: CacheRow;
let wasResolved: boolean;
for (let previousSearch in cache.resultsToSearchCache) {
@@ -287,20 +314,25 @@ export class SearchService implements IRawSearchService {
continue; // since a path character widens the search for potential more matches, require it in previous search too
}
const c = cache.resultsToSearchCache[previousSearch];
c.then(() => { wasResolved = false; });
const row = cache.resultsToSearchCache[previousSearch];
row.promise.then(() => { wasResolved = false; });
wasResolved = true;
cached = this.preventCancellation(c);
cachedRow = {
promise: this.preventCancellation(row.promise),
event: row.event
};
break;
}
}
if (!cached) {
if (!cachedRow) {
return null;
}
return new PPromise<[ISerializedSearchComplete, IRawFileMatch[], CacheStats], IProgress>((c, e, p) => {
cached.then(([complete, cachedEntries]) => {
const listener = cachedRow.event(progressCallback);
return new TPromise<[ISerializedSearchSuccess, IRawFileMatch[], CacheStats]>((c, e) => {
cachedRow.promise.then(([complete, cachedEntries]) => {
const cacheFilterStartTime = Date.now();
// Pattern match on results
@@ -322,21 +354,22 @@ export class SearchService implements IRawSearchService {
cacheFilterStartTime: cacheFilterStartTime,
cacheFilterResultCount: cachedEntries.length
}]);
}, e, p);
}, e);
}, () => {
cached.cancel();
cachedRow.promise.cancel();
listener.dispose();
});
}
private doTextSearch(engine: TextSearchEngine, batchSize: number): PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem> {
return new PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem>((c, e, p) => {
private doTextSearch(engine: TextSearchEngine, progressCallback: IProgressCallback, batchSize: number): TPromise<ISerializedSearchSuccess> {
return new TPromise<ISerializedSearchSuccess>((c, e) => {
// Use BatchedCollector to get new results to the frontend every 2s at least, until 50 results have been returned
const collector = new BatchedCollector<ISerializedFileMatch>(batchSize, p);
const collector = new BatchedCollector<ISerializedFileMatch>(batchSize, progressCallback);
engine.search((matches) => {
const totalMatches = matches.reduce((acc, m) => acc + m.numMatches, 0);
collector.addItems(matches, totalMatches);
}, (progress) => {
p(progress);
progressCallback(progress);
}, (error, stats) => {
collector.flush();
@@ -351,28 +384,28 @@ export class SearchService implements IRawSearchService {
});
}
private doSearch(engine: ISearchEngine<IRawFileMatch>, batchSize?: number): PPromise<ISerializedSearchComplete, IFileSearchProgressItem> {
return new PPromise<ISerializedSearchComplete, IFileSearchProgressItem>((c, e, p) => {
private doSearch(engine: ISearchEngine<IRawFileMatch>, progressCallback: IFileProgressCallback, batchSize?: number): TPromise<ISerializedSearchSuccess> {
return new TPromise<ISerializedSearchSuccess>((c, e) => {
let batch: IRawFileMatch[] = [];
engine.search((match) => {
if (match) {
if (batchSize) {
batch.push(match);
if (batchSize > 0 && batch.length >= batchSize) {
p(batch);
progressCallback(batch);
batch = [];
}
} else {
p(match);
progressCallback(match);
}
}
}, (progress) => {
process.nextTick(() => {
p(progress);
progressCallback(progress);
});
}, (error, stats) => {
if (batch.length) {
p(batch);
progressCallback(batch);
}
if (error) {
e(error);
@@ -390,19 +423,11 @@ export class SearchService implements IRawSearchService {
return TPromise.as(undefined);
}
public fetchTelemetry(): PPromise<void, ITelemetryEvent> {
return new PPromise((c, e, p) => {
this.telemetryPipe = p;
}, () => {
this.telemetryPipe = null;
});
}
private preventCancellation<C, P>(promise: PPromise<C, P>): PPromise<C, P> {
return new PPromise<C, P>((c, e, p) => {
private preventCancellation<C, P>(promise: TPromise<C>): TPromise<C> {
return new TPromise<C>((c, e) => {
// Allow for piled up cancellations to come through first.
process.nextTick(() => {
promise.then(c, e, p);
promise.then(c, e);
});
}, () => {
// Do not propagate.
@@ -410,9 +435,14 @@ export class SearchService implements IRawSearchService {
}
}
interface CacheRow {
promise: TPromise<[ISerializedSearchSuccess, IRawFileMatch[]]>;
event: Event<IFileSearchProgressItem>;
}
class Cache {
public resultsToSearchCache: { [searchValue: string]: PPromise<[ISerializedSearchComplete, IRawFileMatch[]], IFileSearchProgressItem>; } = Object.create(null);
public resultsToSearchCache: { [searchValue: string]: CacheRow; } = Object.create(null);
public scorerCache: ScorerCache = Object.create(null);
}

View File

@@ -8,7 +8,8 @@ import { rgPath } from 'vscode-ripgrep';
import { isMacintosh as isMac } from 'vs/base/common/platform';
import * as glob from 'vs/base/common/glob';
import { normalizeNFD, startsWith } from 'vs/base/common/strings';
import { startsWith } from 'vs/base/common/strings';
import { normalizeNFD } from 'vs/base/common/normalization';
import { IFolderSearch, IRawSearch } from './search';
import { foldersToIncludeGlobs, foldersToRgExcludeGlobs } from './ripgrepTextSearch';
@@ -83,4 +84,4 @@ function getRgArgs(config: IRawSearch, folderQuery: IFolderSearch, includePatter
function anchor(glob: string) {
return startsWith(glob, '**') || startsWith(glob, '/') ? glob : `/${glob}`;
}
}

View File

@@ -4,24 +4,21 @@
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as cp from 'child_process';
import { EventEmitter } from 'events';
import * as path from 'path';
import { StringDecoder, NodeStringDecoder } from 'string_decoder';
import * as cp from 'child_process';
import { rgPath } from 'vscode-ripgrep';
import { NodeStringDecoder, StringDecoder } from 'string_decoder';
import * as glob from 'vs/base/common/glob';
import * as objects from 'vs/base/common/objects';
import * as paths from 'vs/base/common/paths';
import * as platform from 'vs/base/common/platform';
import * as strings from 'vs/base/common/strings';
import * as paths from 'vs/base/common/paths';
import * as extfs from 'vs/base/node/extfs';
import * as encoding from 'vs/base/node/encoding';
import * as glob from 'vs/base/common/glob';
import { TPromise } from 'vs/base/common/winjs.base';
import { ISerializedFileMatch, ISerializedSearchComplete, IRawSearch, IFolderSearch, LineMatch, FileMatch } from './search';
import * as encoding from 'vs/base/node/encoding';
import * as extfs from 'vs/base/node/extfs';
import { IProgress } from 'vs/platform/search/common/search';
import { rgPath } from 'vscode-ripgrep';
import { FileMatch, IFolderSearch, IRawSearch, ISerializedFileMatch, LineMatch, ISerializedSearchSuccess } from './search';
// If vscode-ripgrep is in an .asar file, then the binary is unpacked.
const rgDiskPath = rgPath.replace(/\bnode_modules\.asar\b/, 'node_modules.asar.unpacked');
@@ -47,10 +44,11 @@ export class RipgrepEngine {
}
// TODO@Rob - make promise-based once the old search is gone, and I don't need them to have matching interfaces anymore
search(onResult: (match: ISerializedFileMatch) => void, onMessage: (message: IProgress) => void, done: (error: Error, complete: ISerializedSearchComplete) => void): void {
search(onResult: (match: ISerializedFileMatch) => void, onMessage: (message: IProgress) => void, done: (error: Error, complete: ISerializedSearchSuccess) => void): void {
if (!this.config.folderQueries.length && !this.config.extraFiles.length) {
process.removeListener('exit', this.killRgProcFn);
done(null, {
type: 'success',
limitHit: false,
stats: null
});
@@ -81,7 +79,7 @@ export class RipgrepEngine {
this.ripgrepParser = new RipgrepParser(this.config.maxResults, cwd, this.config.extraFiles);
this.ripgrepParser.on('result', (match: ISerializedFileMatch) => {
if (this.postProcessExclusions) {
const handleResultP = (<TPromise<string>>this.postProcessExclusions(match.path, undefined, () => getSiblings(match.path)))
const handleResultP = (<TPromise<string>>this.postProcessExclusions(match.path, undefined, glob.hasSiblingPromiseFn(() => getSiblings(match.path))))
.then(globMatch => {
if (!globMatch) {
onResult(match);
@@ -97,6 +95,7 @@ export class RipgrepEngine {
this.cancel();
process.removeListener('exit', this.killRgProcFn);
done(null, {
type: 'success',
limitHit: true,
stats: null
});
@@ -127,11 +126,13 @@ export class RipgrepEngine {
process.removeListener('exit', this.killRgProcFn);
if (stderr && !gotData && (displayMsg = rgErrorMsgForDisplay(stderr))) {
done(new Error(displayMsg), {
type: 'success',
limitHit: false,
stats: null
});
} else {
done(null, {
type: 'success',
limitHit: false,
stats: null
});
@@ -148,7 +149,7 @@ export class RipgrepEngine {
* "failed" when a fatal error was produced.
*/
export function rgErrorMsgForDisplay(msg: string): string | undefined {
const firstLine = msg.split('\n')[0];
const firstLine = msg.split('\n')[0].trim();
if (strings.startsWith(firstLine, 'Error parsing regex')) {
return firstLine;
@@ -160,8 +161,13 @@ export function rgErrorMsgForDisplay(msg: string): string | undefined {
return firstLine.charAt(0).toUpperCase() + firstLine.substr(1);
}
if (firstLine === `Literal '\\n' not allowed.`) {
// I won't localize this because none of the Ripgrep error messages are localized
return `Literal '\\n' currently not supported`;
}
if (strings.startsWith(firstLine, 'Literal ')) {
// e.g. "Literal \n not allowed"
// Other unsupported chars
return firstLine;
}
@@ -469,9 +475,11 @@ function getRgArgs(config: IRawSearch) {
args.push('--follow');
}
// Set default encoding if only one folder is opened
if (config.folderQueries.length === 1 && config.folderQueries[0].fileEncoding && config.folderQueries[0].fileEncoding !== 'utf8') {
args.push('--encoding', encoding.toCanonicalName(config.folderQueries[0].fileEncoding));
if (config.folderQueries[0]) {
const folder0Encoding = config.folderQueries[0].fileEncoding;
if (folder0Encoding && folder0Encoding !== 'utf8' && config.folderQueries.every(fq => fq.fileEncoding === folder0Encoding)) {
args.push('--encoding', encoding.toCanonicalName(folder0Encoding));
}
}
// Ripgrep handles -- as a -- arg separator. Only --.
@@ -487,12 +495,14 @@ function getRgArgs(config: IRawSearch) {
const regexpStr = regexp.source.replace(/\\\//g, '/'); // RegExp.source arbitrarily returns escaped slashes. Search and destroy.
args.push('--regexp', regexpStr);
} else if (config.contentPattern.isRegExp) {
args.push('--regexp', config.contentPattern.pattern);
args.push('--regexp', fixRegexEndingPattern(config.contentPattern.pattern));
} else {
searchPatternAfterDoubleDashes = config.contentPattern.pattern;
args.push('--fixed-strings');
}
args.push('--no-config');
// Folder to search
args.push('--');
@@ -539,3 +549,12 @@ function findUniversalExcludes(folderQueries: IFolderSearch[]): Set<string> {
return universalExcludes;
}
// Exported for testing
export function fixRegexEndingPattern(pattern: string): string {
// Replace an unescaped $ at the end of the pattern with \r?$
// Match $ preceeded by none or even number of literal \
return pattern.match(/([^\\]|^)(\\\\)*\$$/) ?
pattern.replace(/\$$/, '\\r?$') :
pattern;
}

View File

@@ -5,10 +5,11 @@
'use strict';
import { PPromise, TPromise } from 'vs/base/common/winjs.base';
import { TPromise } from 'vs/base/common/winjs.base';
import { IExpression } from 'vs/base/common/glob';
import { IProgress, ILineMatch, IPatternInfo, ISearchStats } from 'vs/platform/search/common/search';
import { ITelemetryData } from 'vs/platform/telemetry/common/telemetry';
import { Event } from 'vs/base/common/event';
export interface IFolderSearch {
folder: string;
@@ -41,10 +42,10 @@ export interface ITelemetryEvent {
}
export interface IRawSearchService {
fileSearch(search: IRawSearch): PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem>;
textSearch(search: IRawSearch): PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem>;
fileSearch(search: IRawSearch): Event<ISerializedSearchProgressItem | ISerializedSearchComplete>;
textSearch(search: IRawSearch): Event<ISerializedSearchProgressItem | ISerializedSearchComplete>;
clearCache(cacheKey: string): TPromise<void>;
fetchTelemetry(): PPromise<void, ITelemetryEvent>;
readonly onTelemetry: Event<ITelemetryEvent>;
}
export interface IRawFileMatch {
@@ -55,15 +56,40 @@ export interface IRawFileMatch {
}
export interface ISearchEngine<T> {
search: (onResult: (matches: T) => void, onProgress: (progress: IProgress) => void, done: (error: Error, complete: ISerializedSearchComplete) => void) => void;
search: (onResult: (matches: T) => void, onProgress: (progress: IProgress) => void, done: (error: Error, complete: ISerializedSearchSuccess) => void) => void;
cancel: () => void;
}
export interface ISerializedSearchComplete {
export interface ISerializedSearchSuccess {
type: 'success';
limitHit: boolean;
stats: ISearchStats;
}
export interface ISerializedSearchError {
type: 'error';
error: {
message: string,
stack: string
};
}
export type ISerializedSearchComplete = ISerializedSearchSuccess | ISerializedSearchError;
export function isSerializedSearchComplete(arg: ISerializedSearchProgressItem | ISerializedSearchComplete): arg is ISerializedSearchComplete {
if ((arg as any).type === 'error') {
return true;
} else if ((arg as any).type === 'success') {
return true;
} else {
return false;
}
}
export function isSerializedSearchSuccess(arg: ISerializedSearchComplete): arg is ISerializedSearchSuccess {
return arg.type === 'success';
}
export interface ISerializedFileMatch {
path: string;
lineMatches?: ILineMatch[];

View File

@@ -0,0 +1,46 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { Emitter, Event } from 'vs/base/common/event';
import { ISearchHistoryValues, ISearchHistoryService } from 'vs/platform/search/common/search';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
export class SearchHistoryService implements ISearchHistoryService {
public _serviceBrand: any;
private static readonly SEARCH_HISTORY_KEY = 'workbench.search.history';
private readonly _onDidClearHistory: Emitter<void> = new Emitter<void>();
public readonly onDidClearHistory: Event<void> = this._onDidClearHistory.event;
constructor(
@IStorageService private storageService: IStorageService
) { }
public clearHistory(): void {
this.storageService.remove(SearchHistoryService.SEARCH_HISTORY_KEY, StorageScope.WORKSPACE);
this._onDidClearHistory.fire();
}
public load(): ISearchHistoryValues {
let result: ISearchHistoryValues;
const raw = this.storageService.get(SearchHistoryService.SEARCH_HISTORY_KEY, StorageScope.WORKSPACE);
if (raw) {
try {
result = JSON.parse(raw);
} catch (e) {
// Invalid data
}
}
return result || {};
}
public save(history: ISearchHistoryValues): void {
this.storageService.store(SearchHistoryService.SEARCH_HISTORY_KEY, JSON.stringify(history), StorageScope.WORKSPACE);
}
}

View File

@@ -5,15 +5,16 @@
'use strict';
import { PPromise, TPromise } from 'vs/base/common/winjs.base';
import { TPromise } from 'vs/base/common/winjs.base';
import { IChannel } from 'vs/base/parts/ipc/common/ipc';
import { IRawSearchService, IRawSearch, ISerializedSearchComplete, ISerializedSearchProgressItem, ITelemetryEvent } from './search';
import { Event } from 'vs/base/common/event';
export interface ISearchChannel extends IChannel {
call(command: 'fileSearch', search: IRawSearch): PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem>;
call(command: 'textSearch', search: IRawSearch): PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem>;
listen(event: 'telemetry'): Event<ITelemetryEvent>;
listen(event: 'fileSearch', search: IRawSearch): Event<ISerializedSearchProgressItem | ISerializedSearchComplete>;
listen(event: 'textSearch', search: IRawSearch): Event<ISerializedSearchProgressItem | ISerializedSearchComplete>;
call(command: 'clearCache', cacheKey: string): TPromise<void>;
call(command: 'fetchTelemetry'): PPromise<void, ITelemetryEvent>;
call(command: string, arg: any): TPromise<any>;
}
@@ -21,34 +22,38 @@ export class SearchChannel implements ISearchChannel {
constructor(private service: IRawSearchService) { }
call(command: string, arg?: any): TPromise<any> {
switch (command) {
listen<T>(event: string, arg?: any): Event<any> {
switch (event) {
case 'telemetry': return this.service.onTelemetry;
case 'fileSearch': return this.service.fileSearch(arg);
case 'textSearch': return this.service.textSearch(arg);
case 'clearCache': return this.service.clearCache(arg);
case 'fetchTelemetry': return this.service.fetchTelemetry();
}
return undefined;
throw new Error('Event not found');
}
call(command: string, arg?: any): TPromise<any> {
switch (command) {
case 'clearCache': return this.service.clearCache(arg);
}
throw new Error('Call not found');
}
}
export class SearchChannelClient implements IRawSearchService {
get onTelemetry(): Event<ITelemetryEvent> { return this.channel.listen('telemetry'); }
constructor(private channel: ISearchChannel) { }
fileSearch(search: IRawSearch): PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem> {
return this.channel.call('fileSearch', search);
fileSearch(search: IRawSearch): Event<ISerializedSearchProgressItem | ISerializedSearchComplete> {
return this.channel.listen('fileSearch', search);
}
textSearch(search: IRawSearch): PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem> {
return this.channel.call('textSearch', search);
textSearch(search: IRawSearch): Event<ISerializedSearchProgressItem | ISerializedSearchComplete> {
return this.channel.listen('textSearch', search);
}
clearCache(cacheKey: string): TPromise<void> {
return this.channel.call('clearCache', cacheKey);
}
fetchTelemetry(): PPromise<void, ITelemetryEvent> {
return this.channel.call('fetchTelemetry');
}
}

View File

@@ -4,33 +4,36 @@
*--------------------------------------------------------------------------------------------*/
'use strict';
import { PPromise, TPromise } from 'vs/base/common/winjs.base';
import uri from 'vs/base/common/uri';
import * as arrays from 'vs/base/common/arrays';
import { Event } from 'vs/base/common/event';
import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { ResourceMap, values } from 'vs/base/common/map';
import { Schemas } from 'vs/base/common/network';
import * as objects from 'vs/base/common/objects';
import * as strings from 'vs/base/common/strings';
import uri from 'vs/base/common/uri';
import { TPromise } from 'vs/base/common/winjs.base';
import * as pfs from 'vs/base/node/pfs';
import { getNextTickChannel } from 'vs/base/parts/ipc/common/ipc';
import { Client, IIPCOptions } from 'vs/base/parts/ipc/node/ipc.cp';
import { IProgress, LineMatch, FileMatch, ISearchComplete, ISearchProgressItem, QueryType, IFileMatch, ISearchQuery, IFolderQuery, ISearchConfiguration, ISearchService, pathIncludedInQuery, ISearchResultProvider } from 'vs/platform/search/common/search';
import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService';
import { IModelService } from 'vs/editor/common/services/modelService';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IRawSearch, ISerializedSearchComplete, ISerializedSearchProgressItem, ISerializedFileMatch, IRawSearchService, ITelemetryEvent } from './search';
import { ISearchChannel, SearchChannelClient } from './searchIpc';
import { IEnvironmentService, IDebugParams } from 'vs/platform/environment/common/environment';
import { ResourceMap } from 'vs/base/common/map';
import { IDisposable } from 'vs/base/common/lifecycle';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { onUnexpectedError } from 'vs/base/common/errors';
import { Schemas } from 'vs/base/common/network';
import * as pfs from 'vs/base/node/pfs';
import { IDebugParams, IEnvironmentService } from 'vs/platform/environment/common/environment';
import { ILogService } from 'vs/platform/log/common/log';
import { FileMatch, IFileMatch, IFolderQuery, IProgress, ISearchComplete, ISearchConfiguration, ISearchProgressItem, ISearchQuery, ISearchResultProvider, ISearchService, LineMatch, pathIncludedInQuery, QueryType, SearchProviderType } from 'vs/platform/search/common/search';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService';
import { IRawSearch, IRawSearchService, ISerializedFileMatch, ISerializedSearchComplete, ISerializedSearchProgressItem, isSerializedSearchComplete, isSerializedSearchSuccess, ITelemetryEvent } from './search';
import { ISearchChannel, SearchChannelClient } from './searchIpc';
export class SearchService implements ISearchService {
export class SearchService extends Disposable implements ISearchService {
public _serviceBrand: any;
private diskSearch: DiskSearch;
private readonly searchProvider: ISearchResultProvider[] = [];
private forwardingTelemetry: PPromise<void, ITelemetryEvent>;
private readonly fileSearchProviders = new Map<string, ISearchResultProvider>();
private readonly textSearchProviders = new Map<string, ISearchResultProvider>();
private readonly fileIndexProviders = new Map<string, ISearchResultProvider>();
constructor(
@IModelService private modelService: IModelService,
@@ -38,22 +41,31 @@ export class SearchService implements ISearchService {
@IEnvironmentService environmentService: IEnvironmentService,
@ITelemetryService private telemetryService: ITelemetryService,
@IConfigurationService private configurationService: IConfigurationService,
@ILogService private logService: ILogService
@ILogService private logService: ILogService,
@IExtensionService private extensionService: IExtensionService
) {
super();
this.diskSearch = new DiskSearch(!environmentService.isBuilt || environmentService.verbose, /*timeout=*/undefined, environmentService.debugSearch);
this.registerSearchResultProvider(this.diskSearch);
this._register(this.diskSearch.onTelemetry(event => {
this.telemetryService.publicLog(event.eventName, event.data);
}));
}
public registerSearchResultProvider(provider: ISearchResultProvider): IDisposable {
this.searchProvider.push(provider);
return {
dispose: () => {
const idx = this.searchProvider.indexOf(provider);
if (idx >= 0) {
this.searchProvider.splice(idx, 1);
}
}
};
public registerSearchResultProvider(scheme: string, type: SearchProviderType, provider: ISearchResultProvider): IDisposable {
let list: Map<string, ISearchResultProvider>;
if (type === SearchProviderType.file) {
list = this.fileSearchProviders;
} else if (type === SearchProviderType.text) {
list = this.textSearchProviders;
} else if (type === SearchProviderType.fileIndex) {
list = this.fileIndexProviders;
}
list.set(scheme, provider);
return toDisposable(() => {
list.delete(scheme);
});
}
public extendQuery(query: ISearchQuery): void {
@@ -78,44 +90,66 @@ export class SearchService implements ISearchService {
}
}
public search(query: ISearchQuery): PPromise<ISearchComplete, ISearchProgressItem> {
this.forwardTelemetry();
public search(query: ISearchQuery, onProgress?: (item: ISearchProgressItem) => void): TPromise<ISearchComplete> {
let combinedPromise: TPromise<void>;
return new PPromise<ISearchComplete, ISearchProgressItem>((onComplete, onError, onProgress) => {
return new TPromise<ISearchComplete>((onComplete, onError) => {
// Get local results from dirty/untitled
const localResults = this.getLocalResults(query);
// Allow caller to register progress callback
process.nextTick(() => localResults.values().filter((res) => !!res).forEach(onProgress));
if (onProgress) {
localResults.values().filter((res) => !!res).forEach(onProgress);
}
this.logService.trace('SearchService#search', JSON.stringify(query));
const providerPromises = this.searchProvider.map(provider => TPromise.wrap(provider.search(query)).then(e => e,
err => {
// TODO@joh
// single provider fail. fail all?
onError(err);
},
progress => {
if (progress.resource) {
// Match
if (!localResults.has(progress.resource)) { // don't override local results
onProgress(progress);
}
} else {
// Progress
onProgress(<IProgress>progress);
}
if (progress.message) {
this.logService.debug('SearchService#search', progress.message);
const onProviderProgress = progress => {
if (progress.resource) {
// Match
if (!localResults.has(progress.resource) && onProgress) { // don't override local results
onProgress(progress);
}
} else if (onProgress) {
// Progress
onProgress(<IProgress>progress);
}
));
combinedPromise = TPromise.join(providerPromises).then(values => {
if (progress.message) {
this.logService.debug('SearchService#search', progress.message);
}
};
const startTime = Date.now();
const schemesInQuery = query.folderQueries.map(fq => fq.folder.scheme);
const providerActivations = schemesInQuery.map(scheme => this.extensionService.activateByEvent(`onSearch:${scheme}`));
const providerPromise = TPromise.join(providerActivations)
.then(() => this.searchWithProviders(query, onProviderProgress))
.then(completes => {
completes = completes.filter(c => !!c);
if (!completes.length) {
return null;
}
return <ISearchComplete>{
limitHit: completes[0] && completes[0].limitHit,
stats: completes[0].stats,
results: arrays.flatten(completes.map(c => c.results))
};
}, errs => {
if (!Array.isArray(errs)) {
errs = [errs];
}
errs = errs.filter(e => !!e);
return TPromise.wrapError(errs[0]);
});
combinedPromise = providerPromise.then(value => {
this.logService.debug(`SearchService#search: ${Date.now() - startTime}ms`);
const values = [value];
const result: ISearchComplete = {
limitHit: false,
@@ -147,6 +181,48 @@ export class SearchService implements ISearchService {
}, () => combinedPromise && combinedPromise.cancel());
}
private searchWithProviders(query: ISearchQuery, onProviderProgress: (progress: ISearchProgressItem) => void) {
const diskSearchQueries: IFolderQuery[] = [];
const searchPs = [];
query.folderQueries.forEach(fq => {
let provider = query.type === QueryType.File ?
this.fileSearchProviders.get(fq.folder.scheme) || this.fileIndexProviders.get(fq.folder.scheme) :
this.textSearchProviders.get(fq.folder.scheme);
if (!provider && fq.folder.scheme === 'file') {
diskSearchQueries.push(fq);
} else if (!provider) {
throw new Error('No search provider registered for scheme: ' + fq.folder.scheme);
} else {
const oneFolderQuery = {
...query,
...{
folderQueries: [fq]
}
};
searchPs.push(provider.search(oneFolderQuery, onProviderProgress));
}
});
const diskSearchExtraFileResources = query.extraFileResources && query.extraFileResources.filter(res => res.scheme === 'file');
if (diskSearchQueries.length || diskSearchExtraFileResources) {
const diskSearchQuery: ISearchQuery = {
...query,
...{
folderQueries: diskSearchQueries
},
extraFileResources: diskSearchExtraFileResources
};
searchPs.push(this.diskSearch.search(diskSearchQuery, onProviderProgress));
}
return TPromise.join(searchPs);
}
private getLocalResults(query: ISearchQuery): ResourceMap<IFileMatch> {
const localResults = new ResourceMap<IFileMatch>();
@@ -218,16 +294,13 @@ export class SearchService implements ISearchService {
}
public clearCache(cacheKey: string): TPromise<void> {
return this.diskSearch.clearCache(cacheKey);
}
const clearPs = [
this.diskSearch,
...values(this.fileIndexProviders)
].map(provider => provider && provider.clearCache(cacheKey));
private forwardTelemetry() {
if (!this.forwardingTelemetry) {
this.forwardingTelemetry = this.diskSearch.fetchTelemetry()
.then(null, onUnexpectedError, event => {
this.telemetryService.publicLog(event.eventName, event.data);
});
}
return TPromise.join(clearPs)
.then(() => { });
}
}
@@ -268,21 +341,25 @@ export class DiskSearch implements ISearchResultProvider {
this.raw = new SearchChannelClient(channel);
}
public search(query: ISearchQuery): PPromise<ISearchComplete, ISearchProgressItem> {
public get onTelemetry(): Event<ITelemetryEvent> {
return this.raw.onTelemetry;
}
public search(query: ISearchQuery, onProgress?: (p: ISearchProgressItem) => void): TPromise<ISearchComplete> {
const folderQueries = query.folderQueries || [];
return TPromise.join(folderQueries.map(q => q.folder.scheme === Schemas.file && pfs.exists(q.folder.fsPath)))
.then(exists => {
const existingFolders = folderQueries.filter((q, index) => exists[index]);
const rawSearch = this.rawSearchQuery(query, existingFolders);
let request: PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem>;
let event: Event<ISerializedSearchProgressItem | ISerializedSearchComplete>;
if (query.type === QueryType.File) {
request = this.raw.fileSearch(rawSearch);
event = this.raw.fileSearch(rawSearch);
} else {
request = this.raw.textSearch(rawSearch);
event = this.raw.textSearch(rawSearch);
}
return DiskSearch.collectResults(request);
return DiskSearch.collectResultsFromEvent(event, onProgress);
});
}
@@ -327,37 +404,52 @@ export class DiskSearch implements ISearchResultProvider {
return rawSearch;
}
public static collectResults(request: PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem>): PPromise<ISearchComplete, ISearchProgressItem> {
public static collectResultsFromEvent(event: Event<ISerializedSearchProgressItem | ISerializedSearchComplete>, onProgress?: (p: ISearchProgressItem) => void): TPromise<ISearchComplete> {
let result: IFileMatch[] = [];
return new PPromise<ISearchComplete, ISearchProgressItem>((c, e, p) => {
request.done((complete) => {
c({
limitHit: complete.limitHit,
results: result,
stats: complete.stats
});
}, e, (data) => {
// Matches
if (Array.isArray(data)) {
const fileMatches = data.map(d => this.createFileMatch(d));
result = result.concat(fileMatches);
fileMatches.forEach(p);
}
let listener: IDisposable;
return new TPromise<ISearchComplete>((c, e) => {
listener = event(ev => {
if (isSerializedSearchComplete(ev)) {
if (isSerializedSearchSuccess(ev)) {
c({
limitHit: ev.limitHit,
results: result,
stats: ev.stats
});
} else {
e(ev.error);
}
// Match
else if ((<ISerializedFileMatch>data).path) {
const fileMatch = this.createFileMatch(<ISerializedFileMatch>data);
result.push(fileMatch);
p(fileMatch);
}
listener.dispose();
} else {
// Matches
if (Array.isArray(ev)) {
const fileMatches = ev.map(d => this.createFileMatch(d));
result = result.concat(fileMatches);
if (onProgress) {
fileMatches.forEach(onProgress);
}
}
// Progress
else {
p(<IProgress>data);
// Match
else if ((<ISerializedFileMatch>ev).path) {
const fileMatch = this.createFileMatch(<ISerializedFileMatch>ev);
result.push(fileMatch);
if (onProgress) {
onProgress(fileMatch);
}
}
// Progress
else if (onProgress) {
onProgress(<IProgress>ev);
}
}
});
}, () => request.cancel());
},
() => listener && listener.dispose());
}
private static createFileMatch(data: ISerializedFileMatch): FileMatch {
@@ -373,8 +465,4 @@ export class DiskSearch implements ISearchResultProvider {
public clearCache(cacheKey: string): TPromise<void> {
return this.raw.clearCache(cacheKey);
}
public fetchTelemetry(): PPromise<void, ITelemetryEvent> {
return this.raw.fetchTelemetry();
}
}

View File

@@ -11,7 +11,7 @@ import { onUnexpectedError } from 'vs/base/common/errors';
import { IProgress } from 'vs/platform/search/common/search';
import { FileWalker } from 'vs/workbench/services/search/node/fileSearch';
import { ISerializedFileMatch, ISerializedSearchComplete, IRawSearch, ISearchEngine } from './search';
import { ISerializedFileMatch, IRawSearch, ISearchEngine, ISerializedSearchSuccess } from './search';
import { ISearchWorker } from './worker/searchWorkerIpc';
import { ITextSearchWorkerProvider } from './textSearchWorkerProvider';
@@ -60,7 +60,7 @@ export class Engine implements ISearchEngine<ISerializedFileMatch[]> {
});
}
search(onResult: (match: ISerializedFileMatch[]) => void, onProgress: (progress: IProgress) => void, done: (error: Error, complete: ISerializedSearchComplete) => void): void {
search(onResult: (match: ISerializedFileMatch[]) => void, onProgress: (progress: IProgress) => void, done: (error: Error, complete: ISerializedSearchSuccess) => void): void {
this.workers = this.workerProvider.getWorkers();
this.initializeWorkers();
@@ -86,6 +86,7 @@ export class Engine implements ISearchEngine<ISerializedFileMatch[]> {
if (!this.isDone && this.processedBytes === this.totalBytes && this.walkerIsDone) {
this.isDone = true;
done(this.walkerError, {
type: 'success',
limitHit: this.limitReached,
stats: this.walker.getStats()
});

View File

@@ -10,6 +10,7 @@ import { IChannel } from 'vs/base/parts/ipc/common/ipc';
import { ISerializedFileMatch } from '../search';
import { IPatternInfo } from 'vs/platform/search/common/search';
import { SearchWorker } from './searchWorker';
import { Event } from 'vs/base/common/event';
export interface ISearchWorkerSearchArgs {
pattern: IPatternInfo;
@@ -41,6 +42,10 @@ export class SearchWorkerChannel implements ISearchWorkerChannel {
constructor(private worker: SearchWorker) {
}
listen<T>(event: string, arg?: any): Event<T> {
throw new Error('No events');
}
call(command: string, arg?: any): TPromise<any> {
switch (command) {
case 'initialize': return this.worker.initialize();

View File

@@ -11,7 +11,7 @@ import * as assert from 'assert';
import * as arrays from 'vs/base/common/arrays';
import * as platform from 'vs/base/common/platform';
import { RipgrepParser, getAbsoluteGlob, fixDriveC } from 'vs/workbench/services/search/node/ripgrepTextSearch';
import { RipgrepParser, getAbsoluteGlob, fixDriveC, fixRegexEndingPattern } from 'vs/workbench/services/search/node/ripgrepTextSearch';
import { ISerializedFileMatch } from 'vs/workbench/services/search/node/search';
@@ -174,7 +174,7 @@ suite('RipgrepParser', () => {
});
});
suite('RipgrepParser - etc', () => {
suite('RipgrepTextSearch - etc', () => {
function testGetAbsGlob(params: string[]): void {
const [folder, glob, expectedResult] = params;
assert.equal(fixDriveC(getAbsoluteGlob(folder, glob)), expectedResult, JSON.stringify(params));
@@ -212,4 +212,20 @@ suite('RipgrepParser - etc', () => {
['/', '/project/folder', '/project/folder'],
].forEach(testGetAbsGlob);
});
test('fixRegexEndingPattern', () => {
function testFixRegexEndingPattern([input, expectedResult]: string[]): void {
assert.equal(fixRegexEndingPattern(input), expectedResult);
}
[
['foo', 'foo'],
['', ''],
['^foo.*bar\\s+', '^foo.*bar\\s+'],
['foo$', 'foo\\r?$'],
['$', '\\r?$'],
['foo\\$', 'foo\\$'],
['foo\\\\$', 'foo\\\\\\r?$'],
].forEach(testFixRegexEndingPattern);
});
});

View File

@@ -578,29 +578,6 @@ suite('FileSearchEngine', () => {
});
});
test('Files: absolute path to file ignores excludes', function (done: () => void) {
this.timeout(testTimeout);
let engine = new FileSearchEngine({
folderQueries: ROOT_FOLDER_QUERY,
filePattern: path.normalize(path.join(require.toUrl('./fixtures'), 'site.css')),
excludePattern: { '**/*.css': true }
});
let count = 0;
let res: IRawFileMatch;
engine.search((result) => {
if (result) {
count++;
}
res = result;
}, () => { }, (error) => {
assert.ok(!error);
assert.equal(count, 1);
assert.equal(path.basename(res.relativePath), 'site.css');
done();
});
});
test('Files: relative path matched once', function (done: () => void) {
this.timeout(testTimeout);
let engine = new FileSearchEngine({
@@ -623,29 +600,6 @@ suite('FileSearchEngine', () => {
});
});
test('Files: relative path to file ignores excludes', function (done: () => void) {
this.timeout(testTimeout);
let engine = new FileSearchEngine({
folderQueries: ROOT_FOLDER_QUERY,
filePattern: path.normalize(path.join('examples', 'company.js')),
excludePattern: { '**/*.js': true }
});
let count = 0;
let res: IRawFileMatch;
engine.search((result) => {
if (result) {
count++;
}
res = result;
}, () => { }, (error) => {
assert.ok(!error);
assert.equal(count, 1);
assert.equal(path.basename(res.relativePath), 'company.js');
done();
});
});
test('Files: Include pattern, single files', function (done: () => void) {
this.timeout(testTimeout);
let engine = new FileSearchEngine({

View File

@@ -9,9 +9,11 @@ import * as assert from 'assert';
import * as path from 'path';
import { IProgress, IUncachedSearchStats } from 'vs/platform/search/common/search';
import { ISearchEngine, IRawSearch, IRawFileMatch, ISerializedFileMatch, ISerializedSearchComplete, IFolderSearch } from 'vs/workbench/services/search/node/search';
import { ISearchEngine, IRawSearch, IRawFileMatch, ISerializedFileMatch, IFolderSearch, ISerializedSearchSuccess, ISerializedSearchProgressItem, ISerializedSearchComplete } from 'vs/workbench/services/search/node/search';
import { SearchService as RawSearchService } from 'vs/workbench/services/search/node/rawSearchService';
import { DiskSearch } from 'vs/workbench/services/search/node/searchService';
import { Emitter, Event } from 'vs/base/common/event';
import { TPromise } from 'vs/base/common/winjs.base';
const TEST_FOLDER_QUERIES = [
{ folder: path.normalize('/some/where') }
@@ -44,12 +46,13 @@ class TestSearchEngine implements ISearchEngine<IRawFileMatch> {
TestSearchEngine.last = this;
}
public search(onResult: (match: IRawFileMatch) => void, onProgress: (progress: IProgress) => void, done: (error: Error, complete: ISerializedSearchComplete) => void): void {
public search(onResult: (match: IRawFileMatch) => void, onProgress: (progress: IProgress) => void, done: (error: Error, complete: ISerializedSearchSuccess) => void): void {
const self = this;
(function next() {
process.nextTick(() => {
if (self.isCanceled) {
done(null, {
type: 'success',
limitHit: false,
stats: stats
});
@@ -58,6 +61,7 @@ class TestSearchEngine implements ISearchEngine<IRawFileMatch> {
const result = self.result();
if (!result) {
done(null, {
type: 'success',
limitHit: false,
stats: stats
});
@@ -101,17 +105,17 @@ suite('SearchService', () => {
const service = new RawSearchService();
let results = 0;
return service.doFileSearch(Engine, rawSearch)
.then(() => {
assert.strictEqual(results, 5);
}, null, value => {
if (!Array.isArray(value)) {
assert.deepStrictEqual(value, match);
results++;
} else {
assert.fail(JSON.stringify(value));
}
});
const cb: (p: ISerializedSearchProgressItem) => void = value => {
if (!Array.isArray(value)) {
assert.deepStrictEqual(value, match);
results++;
} else {
assert.fail(JSON.stringify(value));
}
};
return service.doFileSearch(Engine, rawSearch, cb)
.then(() => assert.strictEqual(results, 5));
});
test('Batch results', function () {
@@ -121,19 +125,20 @@ suite('SearchService', () => {
const service = new RawSearchService();
const results = [];
return service.doFileSearch(Engine, rawSearch, 10)
.then(() => {
assert.deepStrictEqual(results, [10, 10, 5]);
}, null, value => {
if (Array.isArray(value)) {
value.forEach(m => {
assert.deepStrictEqual(m, match);
});
results.push(value.length);
} else {
assert.fail(JSON.stringify(value));
}
});
const cb: (p: ISerializedSearchProgressItem) => void = value => {
if (Array.isArray(value)) {
value.forEach(m => {
assert.deepStrictEqual(m, match);
});
results.push(value.length);
} else {
assert.fail(JSON.stringify(value));
}
};
return service.doFileSearch(Engine, rawSearch, cb, 10).then(() => {
assert.deepStrictEqual(results, [10, 10, 5]);
});
});
test('Collect batched results', function () {
@@ -143,14 +148,32 @@ suite('SearchService', () => {
const Engine = TestSearchEngine.bind(null, () => i-- && rawMatch);
const service = new RawSearchService();
function fileSearch(config: IRawSearch, batchSize: number): Event<ISerializedSearchProgressItem | ISerializedSearchComplete> {
let promise: TPromise<ISerializedSearchSuccess>;
const emitter = new Emitter<ISerializedSearchProgressItem | ISerializedSearchComplete>({
onFirstListenerAdd: () => {
promise = service.doFileSearch(Engine, config, p => emitter.fire(p), batchSize)
.then(c => emitter.fire(c), err => emitter.fire({ type: 'error', error: err }));
},
onLastListenerRemove: () => {
promise.cancel();
}
});
return emitter.event;
}
const progressResults = [];
return DiskSearch.collectResults(service.doFileSearch(Engine, rawSearch, 10))
const onProgress = match => {
assert.strictEqual(match.resource.path, uriPath);
progressResults.push(match);
};
return DiskSearch.collectResultsFromEvent(fileSearch(rawSearch, 10), onProgress)
.then(result => {
assert.strictEqual(result.results.length, 25, 'Result');
assert.strictEqual(progressResults.length, 25, 'Progress');
}, null, match => {
assert.strictEqual(match.resource.path, uriPath);
progressResults.push(match);
});
});
@@ -167,7 +190,7 @@ suite('SearchService', () => {
},
};
return DiskSearch.collectResults(service.fileSearch(query))
return DiskSearch.collectResultsFromEvent(service.fileSearch(query))
.then(result => {
assert.strictEqual(result.results.length, 1, 'Result');
});
@@ -186,7 +209,7 @@ suite('SearchService', () => {
},
};
return DiskSearch.collectResults(service.fileSearch(query))
return DiskSearch.collectResultsFromEvent(service.fileSearch(query))
.then(result => {
assert.strictEqual(result.results.length, 0, 'Result');
assert.ok(result.limitHit);
@@ -206,20 +229,22 @@ suite('SearchService', () => {
const service = new RawSearchService();
const results = [];
return service.doFileSearch(Engine, {
folderQueries: TEST_FOLDER_QUERIES,
filePattern: 'bb',
sortByScore: true,
maxResults: 2
}, 1).then(() => {
assert.notStrictEqual(typeof TestSearchEngine.last.config.maxResults, 'number');
assert.deepStrictEqual(results, [path.normalize('/some/where/bbc'), path.normalize('/some/where/bab')]);
}, null, value => {
const cb = value => {
if (Array.isArray(value)) {
results.push(...value.map(v => v.path));
} else {
assert.fail(JSON.stringify(value));
}
};
return service.doFileSearch(Engine, {
folderQueries: TEST_FOLDER_QUERIES,
filePattern: 'bb',
sortByScore: true,
maxResults: 2
}, cb, 1).then(() => {
assert.notStrictEqual(typeof TestSearchEngine.last.config.maxResults, 'number');
assert.deepStrictEqual(results, [path.normalize('/some/where/bbc'), path.normalize('/some/where/bab')]);
});
});
@@ -230,23 +255,24 @@ suite('SearchService', () => {
const service = new RawSearchService();
const results = [];
const cb = value => {
if (Array.isArray(value)) {
value.forEach(m => {
assert.deepStrictEqual(m, match);
});
results.push(value.length);
} else {
assert.fail(JSON.stringify(value));
}
};
return service.doFileSearch(Engine, {
folderQueries: TEST_FOLDER_QUERIES,
filePattern: 'a',
sortByScore: true,
maxResults: 23
}, 10)
}, cb, 10)
.then(() => {
assert.deepStrictEqual(results, [10, 10, 3]);
}, null, value => {
if (Array.isArray(value)) {
value.forEach(m => {
assert.deepStrictEqual(m, match);
});
results.push(value.length);
} else {
assert.fail(JSON.stringify(value));
}
});
});
@@ -263,37 +289,39 @@ suite('SearchService', () => {
const service = new RawSearchService();
const results = [];
return service.doFileSearch(Engine, {
folderQueries: TEST_FOLDER_QUERIES,
filePattern: 'b',
sortByScore: true,
cacheKey: 'x'
}, -1).then(complete => {
assert.strictEqual(complete.stats.fromCache, false);
assert.deepStrictEqual(results, [path.normalize('/some/where/bcb'), path.normalize('/some/where/bbc'), path.normalize('/some/where/aab')]);
}, null, value => {
const cb = value => {
if (Array.isArray(value)) {
results.push(...value.map(v => v.path));
} else {
assert.fail(JSON.stringify(value));
}
};
return service.doFileSearch(Engine, {
folderQueries: TEST_FOLDER_QUERIES,
filePattern: 'b',
sortByScore: true,
cacheKey: 'x'
}, cb, -1).then(complete => {
assert.strictEqual(complete.stats.fromCache, false);
assert.deepStrictEqual(results, [path.normalize('/some/where/bcb'), path.normalize('/some/where/bbc'), path.normalize('/some/where/aab')]);
}).then(() => {
const results = [];
return service.doFileSearch(Engine, {
folderQueries: TEST_FOLDER_QUERIES,
filePattern: 'bc',
sortByScore: true,
cacheKey: 'x'
}, -1).then(complete => {
assert.ok(complete.stats.fromCache);
assert.deepStrictEqual(results, [path.normalize('/some/where/bcb'), path.normalize('/some/where/bbc')]);
}, null, value => {
const cb = value => {
if (Array.isArray(value)) {
results.push(...value.map(v => v.path));
} else {
assert.fail(JSON.stringify(value));
}
});
};
return service.doFileSearch(Engine, {
folderQueries: TEST_FOLDER_QUERIES,
filePattern: 'bc',
sortByScore: true,
cacheKey: 'x'
}, cb, -1).then(complete => {
assert.ok(complete.stats.fromCache);
assert.deepStrictEqual(results, [path.normalize('/some/where/bcb'), path.normalize('/some/where/bbc')]);
}, null);
}).then(() => {
return service.clearCache('x');
}).then(() => {
@@ -304,20 +332,21 @@ suite('SearchService', () => {
size: 3
});
const results = [];
return service.doFileSearch(Engine, {
folderQueries: TEST_FOLDER_QUERIES,
filePattern: 'bc',
sortByScore: true,
cacheKey: 'x'
}, -1).then(complete => {
assert.strictEqual(complete.stats.fromCache, false);
assert.deepStrictEqual(results, [path.normalize('/some/where/bc')]);
}, null, value => {
const cb = value => {
if (Array.isArray(value)) {
results.push(...value.map(v => v.path));
} else {
assert.fail(JSON.stringify(value));
}
};
return service.doFileSearch(Engine, {
folderQueries: TEST_FOLDER_QUERIES,
filePattern: 'bc',
sortByScore: true,
cacheKey: 'x'
}, cb, -1).then(complete => {
assert.strictEqual(complete.stats.fromCache, false);
assert.deepStrictEqual(results, [path.normalize('/some/where/bc')]);
});
});
});

View File

@@ -7,8 +7,8 @@
import * as nls from 'vs/nls';
import * as dom from 'vs/base/browser/dom';
import * as types from 'vs/base/common/types';
import * as resources from 'vs/base/common/resources';
import { Event, Emitter } from 'vs/base/common/event';
import { join, normalize } from 'path';
import { TPromise } from 'vs/base/common/winjs.base';
import { onUnexpectedError } from 'vs/base/common/errors';
import { ExtensionMessageCollector } from 'vs/workbench/services/extensions/common/extensionsRegistry';
@@ -22,6 +22,10 @@ import { TokenizationResult, TokenizationResult2 } from 'vs/editor/common/core/t
import { nullTokenize2 } from 'vs/editor/common/modes/nullMode';
import { generateTokensCSSForColorMap } from 'vs/editor/common/modes/supports/tokenization';
import { Color } from 'vs/base/common/color';
import { INotificationService } from 'vs/platform/notification/common/notification';
import URI from 'vs/base/common/uri';
import { IFileService } from 'vs/platform/files/common/files';
import { ILogService } from 'vs/platform/log/common/log';
export class TMScopeRegistry {
@@ -36,27 +40,27 @@ export class TMScopeRegistry {
this._encounteredLanguages = [];
}
public register(scopeName: string, filePath: string, embeddedLanguages?: IEmbeddedLanguagesMap, tokenTypes?: TokenTypesContribution): void {
public register(scopeName: string, grammarLocation: URI, embeddedLanguages?: IEmbeddedLanguagesMap, tokenTypes?: TokenTypesContribution): void {
if (this._scopeNameToLanguageRegistration[scopeName]) {
const existingRegistration = this._scopeNameToLanguageRegistration[scopeName];
if (existingRegistration.grammarFilePath !== filePath) {
if (!resources.isEqual(existingRegistration.grammarLocation, grammarLocation)) {
console.warn(
`Overwriting grammar scope name to file mapping for scope ${scopeName}.\n` +
`Old grammar file: ${existingRegistration.grammarFilePath}.\n` +
`New grammar file: ${filePath}`
`Old grammar file: ${existingRegistration.grammarLocation.toString()}.\n` +
`New grammar file: ${grammarLocation.toString()}`
);
}
}
this._scopeNameToLanguageRegistration[scopeName] = new TMLanguageRegistration(scopeName, filePath, embeddedLanguages, tokenTypes);
this._scopeNameToLanguageRegistration[scopeName] = new TMLanguageRegistration(scopeName, grammarLocation, embeddedLanguages, tokenTypes);
}
public getLanguageRegistration(scopeName: string): TMLanguageRegistration {
return this._scopeNameToLanguageRegistration[scopeName] || null;
}
public getFilePath(scopeName: string): string {
public getGrammarLocation(scopeName: string): URI {
let data = this.getLanguageRegistration(scopeName);
return data ? data.grammarFilePath : null;
return data ? data.grammarLocation : null;
}
/**
@@ -74,13 +78,13 @@ export class TMLanguageRegistration {
_topLevelScopeNameDataBrand: void;
readonly scopeName: string;
readonly grammarFilePath: string;
readonly grammarLocation: URI;
readonly embeddedLanguages: IEmbeddedLanguagesMap;
readonly tokenTypes: ITokenTypeMap;
constructor(scopeName: string, grammarFilePath: string, embeddedLanguages: IEmbeddedLanguagesMap, tokenTypes: TokenTypesContribution | undefined) {
constructor(scopeName: string, grammarLocation: URI, embeddedLanguages: IEmbeddedLanguagesMap, tokenTypes: TokenTypesContribution | undefined) {
this.scopeName = scopeName;
this.grammarFilePath = grammarFilePath;
this.grammarLocation = grammarLocation;
// embeddedLanguages handling
this.embeddedLanguages = Object.create(null);
@@ -135,9 +139,12 @@ export class TextMateService implements ITextMateService {
private _grammarRegistry: TPromise<[Registry, StackElement]>;
private _modeService: IModeService;
private _themeService: IWorkbenchThemeService;
private _fileService: IFileService;
private _logService: ILogService;
private _scopeRegistry: TMScopeRegistry;
private _injections: { [scopeName: string]: string[]; };
private _injectedEmbeddedLanguages: { [scopeName: string]: IEmbeddedLanguagesMap[]; };
private _notificationService: INotificationService;
private _languageToScope: Map<string, string>;
private _styleElement: HTMLStyleElement;
@@ -148,17 +155,23 @@ export class TextMateService implements ITextMateService {
constructor(
@IModeService modeService: IModeService,
@IWorkbenchThemeService themeService: IWorkbenchThemeService
@IWorkbenchThemeService themeService: IWorkbenchThemeService,
@IFileService fileService: IFileService,
@INotificationService notificationService: INotificationService,
@ILogService logService: ILogService
) {
this._styleElement = dom.createStyleSheet();
this._styleElement.className = 'vscode-tokens-styles';
this._modeService = modeService;
this._themeService = themeService;
this._fileService = fileService;
this._logService = logService;
this._scopeRegistry = new TMScopeRegistry();
this.onDidEncounterLanguage = this._scopeRegistry.onDidEncounterLanguage;
this._injections = {};
this._injectedEmbeddedLanguages = {};
this._languageToScope = new Map<string, string>();
this._notificationService = notificationService;
this._grammarRegistry = null;
@@ -166,7 +179,7 @@ export class TextMateService implements ITextMateService {
for (let i = 0; i < extensions.length; i++) {
let grammars = extensions[i].value;
for (let j = 0; j < grammars.length; j++) {
this._handleGrammarExtensionPointUser(extensions[i].description.extensionFolderPath, grammars[j], extensions[i].collector);
this._handleGrammarExtensionPointUser(extensions[i].description.extensionLocation, grammars[j], extensions[i].collector);
}
}
});
@@ -198,10 +211,20 @@ export class TextMateService implements ITextMateService {
private _getOrCreateGrammarRegistry(): TPromise<[Registry, StackElement]> {
if (!this._grammarRegistry) {
this._grammarRegistry = TPromise.wrap(import('vscode-textmate')).then(({ Registry, INITIAL }) => {
this._grammarRegistry = TPromise.wrap(import('vscode-textmate')).then(({ Registry, INITIAL, parseRawGrammar }) => {
const grammarRegistry = new Registry({
getFilePath: (scopeName: string) => {
return this._scopeRegistry.getFilePath(scopeName);
loadGrammar: (scopeName: string) => {
const location = this._scopeRegistry.getGrammarLocation(scopeName);
if (!location) {
this._logService.info(`No grammar found for scope ${scopeName}`);
return null;
}
return this._fileService.resolveContent(location, { encoding: 'utf8' }).then(content => {
return parseRawGrammar(content.value, location.path);
}, e => {
this._logService.error(`Unable to load and parse grammar for scope ${scopeName} from ${location}`, e);
return null;
});
},
getInjections: (scopeName: string) => {
return this._injections[scopeName];
@@ -262,7 +285,7 @@ export class TextMateService implements ITextMateService {
}
private _handleGrammarExtensionPointUser(extensionFolderPath: string, syntax: ITMSyntaxExtensionPoint, collector: ExtensionMessageCollector): void {
private _handleGrammarExtensionPointUser(extensionLocation: URI, syntax: ITMSyntaxExtensionPoint, collector: ExtensionMessageCollector): void {
if (syntax.language && ((typeof syntax.language !== 'string') || !this._modeService.isRegisteredMode(syntax.language))) {
collector.error(nls.localize('invalid.language', "Unknown language in `contributes.{0}.language`. Provided value: {1}", grammarsExtPoint.name, String(syntax.language)));
return;
@@ -289,13 +312,12 @@ export class TextMateService implements ITextMateService {
return;
}
let normalizedAbsolutePath = normalize(join(extensionFolderPath, syntax.path));
if (normalizedAbsolutePath.indexOf(extensionFolderPath) !== 0) {
collector.warn(nls.localize('invalid.path.1', "Expected `contributes.{0}.path` ({1}) to be included inside extension's folder ({2}). This might make the extension non-portable.", grammarsExtPoint.name, normalizedAbsolutePath, extensionFolderPath));
const grammarLocation = resources.joinPath(extensionLocation, syntax.path);
if (grammarLocation.path.indexOf(extensionLocation.path) !== 0) {
collector.warn(nls.localize('invalid.path.1', "Expected `contributes.{0}.path` ({1}) to be included inside extension's folder ({2}). This might make the extension non-portable.", grammarsExtPoint.name, grammarLocation.path, extensionLocation.path));
}
this._scopeRegistry.register(syntax.scopeName, normalizedAbsolutePath, syntax.embeddedLanguages, syntax.tokenTypes);
this._scopeRegistry.register(syntax.scopeName, grammarLocation, syntax.embeddedLanguages, syntax.tokenTypes);
if (syntax.injectTo) {
for (let injectScope of syntax.injectTo) {
@@ -349,9 +371,10 @@ export class TextMateService implements ITextMateService {
return TPromise.wrapError<ICreateGrammarResult>(new Error(nls.localize('no-tm-grammar', "No TM Grammar registered for this language.")));
}
let embeddedLanguages = this._resolveEmbeddedLanguages(languageRegistration.embeddedLanguages);
let injectedEmbeddedLanguages = this._injectedEmbeddedLanguages[scopeName];
if (injectedEmbeddedLanguages) {
for (const injected of injectedEmbeddedLanguages.map(this._resolveEmbeddedLanguages.bind(this))) {
let rawInjectedEmbeddedLanguages = this._injectedEmbeddedLanguages[scopeName];
if (rawInjectedEmbeddedLanguages) {
let injectedEmbeddedLanguages: IEmbeddedLanguagesMap2[] = rawInjectedEmbeddedLanguages.map(this._resolveEmbeddedLanguages.bind(this));
for (const injected of injectedEmbeddedLanguages) {
for (const scope of Object.keys(injected)) {
embeddedLanguages[scope] = injected[scope];
}
@@ -362,25 +385,20 @@ export class TextMateService implements ITextMateService {
let containsEmbeddedLanguages = (Object.keys(embeddedLanguages).length > 0);
return this._getOrCreateGrammarRegistry().then((_res) => {
const [grammarRegistry, initialState] = _res;
return new TPromise<ICreateGrammarResult>((c, e, p) => {
grammarRegistry.loadGrammarWithConfiguration(scopeName, languageId, { embeddedLanguages, tokenTypes: languageRegistration.tokenTypes }, (err, grammar) => {
if (err) {
return e(err);
}
c({
languageId: languageId,
grammar: grammar,
initialState: initialState,
containsEmbeddedLanguages: containsEmbeddedLanguages
});
});
return grammarRegistry.loadGrammarWithConfiguration(scopeName, languageId, { embeddedLanguages, tokenTypes: languageRegistration.tokenTypes }).then(grammar => {
return {
languageId: languageId,
grammar: grammar,
initialState: initialState,
containsEmbeddedLanguages: containsEmbeddedLanguages
};
});
});
}
private registerDefinition(modeId: string): void {
this._createGrammar(modeId).then((r) => {
TokenizationRegistry.register(modeId, new TMTokenization(this._scopeRegistry, r.languageId, r.grammar, r.initialState, r.containsEmbeddedLanguages));
TokenizationRegistry.register(modeId, new TMTokenization(this._scopeRegistry, r.languageId, r.grammar, r.initialState, r.containsEmbeddedLanguages, this._notificationService));
}, onUnexpectedError);
}
}
@@ -393,8 +411,9 @@ class TMTokenization implements ITokenizationSupport {
private readonly _containsEmbeddedLanguages: boolean;
private readonly _seenLanguages: boolean[];
private readonly _initialState: StackElement;
private _tokenizationWarningAlreadyShown: boolean;
constructor(scopeRegistry: TMScopeRegistry, languageId: LanguageId, grammar: IGrammar, initialState: StackElement, containsEmbeddedLanguages: boolean) {
constructor(scopeRegistry: TMScopeRegistry, languageId: LanguageId, grammar: IGrammar, initialState: StackElement, containsEmbeddedLanguages: boolean, @INotificationService private notificationService: INotificationService) {
this._scopeRegistry = scopeRegistry;
this._languageId = languageId;
this._grammar = grammar;
@@ -418,6 +437,10 @@ class TMTokenization implements ITokenizationSupport {
// Do not attempt to tokenize if a line has over 20k
if (line.length >= 20000) {
if (!this._tokenizationWarningAlreadyShown) {
this._tokenizationWarningAlreadyShown = true;
this.notificationService.warn(nls.localize('too many characters', "Tokenization is skipped for lines longer than 20k characters for performance reasons."));
}
console.log(`Line (${line.substr(0, 15)}...): longer than 20k characters, tokenization skipped.`);
return nullTokenize2(this._languageId, line, state, offsetDelta);
}

View File

@@ -12,13 +12,11 @@ import { onUnexpectedError } from 'vs/base/common/errors';
import { guessMimeTypes } from 'vs/base/common/mime';
import { toErrorMessage } from 'vs/base/common/errorMessage';
import URI from 'vs/base/common/uri';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import * as diagnostics from 'vs/base/common/diagnostics';
import * as types from 'vs/base/common/types';
import { isUndefinedOrNull } from 'vs/base/common/types';
import { IMode } from 'vs/editor/common/modes';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { ITextFileService, IAutoSaveConfiguration, ModelState, ITextFileEditorModel, ISaveOptions, ISaveErrorHandler, ISaveParticipant, StateChange, SaveReason, IRawTextContent, ILoadOptions } from 'vs/workbench/services/textfile/common/textfiles';
import { ITextFileService, IAutoSaveConfiguration, ModelState, ITextFileEditorModel, ISaveOptions, ISaveErrorHandler, ISaveParticipant, StateChange, SaveReason, IRawTextContent, ILoadOptions, LoadReason } from 'vs/workbench/services/textfile/common/textfiles';
import { EncodingMode } from 'vs/workbench/common/editor';
import { BaseTextEditorModel } from 'vs/workbench/common/editor/textEditorModel';
import { IBackupFileService } from 'vs/workbench/services/backup/common/backup';
@@ -32,17 +30,32 @@ import { ITextBufferFactory } from 'vs/editor/common/model';
import { IHashService } from 'vs/workbench/services/hash/common/hashService';
import { createTextBufferFactory } from 'vs/editor/common/model/textModel';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { isLinux } from 'vs/base/common/platform';
import { IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { ILogService } from 'vs/platform/log/common/log';
import { isEqual, isEqualOrParent, hasToIgnoreCase } from 'vs/base/common/resources';
/**
* The text file editor model listens to changes to its underlying code editor model and saves these changes through the file service back to the disk.
*/
export class TextFileEditorModel extends BaseTextEditorModel implements ITextFileEditorModel {
public static DEFAULT_CONTENT_CHANGE_BUFFER_DELAY = CONTENT_CHANGE_EVENT_BUFFER_DELAY;
public static DEFAULT_ORPHANED_CHANGE_BUFFER_DELAY = 100;
static DEFAULT_CONTENT_CHANGE_BUFFER_DELAY = CONTENT_CHANGE_EVENT_BUFFER_DELAY;
static DEFAULT_ORPHANED_CHANGE_BUFFER_DELAY = 100;
static WHITELIST_JSON = ['package.json', 'package-lock.json', 'tsconfig.json', 'jsconfig.json', 'bower.json', '.eslintrc.json', 'tslint.json', 'composer.json'];
static WHITELIST_WORKSPACE_JSON = ['settings.json', 'extensions.json', 'tasks.json', 'launch.json'];
private static saveErrorHandler: ISaveErrorHandler;
static setSaveErrorHandler(handler: ISaveErrorHandler): void { TextFileEditorModel.saveErrorHandler = handler; }
private static saveParticipant: ISaveParticipant;
static setSaveParticipant(handler: ISaveParticipant): void { TextFileEditorModel.saveParticipant = handler; }
private readonly _onDidContentChange: Emitter<StateChange> = this._register(new Emitter<StateChange>());
get onDidContentChange(): Event<StateChange> { return this._onDidContentChange.event; }
private readonly _onDidStateChange: Emitter<StateChange> = this._register(new Emitter<StateChange>());
get onDidStateChange(): Event<StateChange> { return this._onDidStateChange.event; }
private resource: URI;
private contentEncoding: string; // encoding as reported from disk
@@ -51,20 +64,16 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
private versionId: number;
private bufferSavedVersionId: number;
private lastResolvedDiskStat: IFileStat;
private toDispose: IDisposable[];
private blockModelContentChange: boolean;
private autoSaveAfterMillies: number;
private autoSaveAfterMilliesEnabled: boolean;
private autoSavePromise: TPromise<void>;
private autoSaveDisposable: IDisposable;
private contentChangeEventScheduler: RunOnceScheduler;
private orphanedChangeEventScheduler: RunOnceScheduler;
private saveSequentializer: SaveSequentializer;
private disposed: boolean;
private lastSaveAttemptTime: number;
private createTextEditorModelPromise: TPromise<TextFileEditorModel>;
private readonly _onDidContentChange: Emitter<StateChange>;
private readonly _onDidStateChange: Emitter<StateChange>;
private inConflictMode: boolean;
private inOrphanMode: boolean;
private inErrorMode: boolean;
@@ -82,15 +91,12 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
@IBackupFileService private backupFileService: IBackupFileService,
@IEnvironmentService private environmentService: IEnvironmentService,
@IWorkspaceContextService private contextService: IWorkspaceContextService,
@IHashService private hashService: IHashService
@IHashService private hashService: IHashService,
@ILogService private logService: ILogService
) {
super(modelService, modeService);
this.resource = resource;
this.toDispose = [];
this._onDidContentChange = new Emitter<StateChange>();
this._onDidStateChange = new Emitter<StateChange>();
this.toDispose.push(this._onDidContentChange);
this.toDispose.push(this._onDidStateChange);
this.preferredEncoding = preferredEncoding;
this.inOrphanMode = false;
this.dirty = false;
@@ -98,11 +104,8 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
this.lastSaveAttemptTime = 0;
this.saveSequentializer = new SaveSequentializer();
this.contentChangeEventScheduler = new RunOnceScheduler(() => this._onDidContentChange.fire(StateChange.CONTENT_CHANGE), TextFileEditorModel.DEFAULT_CONTENT_CHANGE_BUFFER_DELAY);
this.toDispose.push(this.contentChangeEventScheduler);
this.orphanedChangeEventScheduler = new RunOnceScheduler(() => this._onDidStateChange.fire(StateChange.ORPHANED_CHANGE), TextFileEditorModel.DEFAULT_ORPHANED_CHANGE_BUFFER_DELAY);
this.toDispose.push(this.orphanedChangeEventScheduler);
this.contentChangeEventScheduler = this._register(new RunOnceScheduler(() => this._onDidContentChange.fire(StateChange.CONTENT_CHANGE), TextFileEditorModel.DEFAULT_CONTENT_CHANGE_BUFFER_DELAY));
this.orphanedChangeEventScheduler = this._register(new RunOnceScheduler(() => this._onDidStateChange.fire(StateChange.ORPHANED_CHANGE), TextFileEditorModel.DEFAULT_ORPHANED_CHANGE_BUFFER_DELAY));
this.updateAutoSaveConfiguration(textFileService.getAutoSaveConfiguration());
@@ -110,10 +113,10 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
}
private registerListeners(): void {
this.toDispose.push(this.fileService.onFileChanges(e => this.onFileChanges(e)));
this.toDispose.push(this.textFileService.onAutoSaveConfigurationChange(config => this.updateAutoSaveConfiguration(config)));
this.toDispose.push(this.textFileService.onFilesAssociationChange(e => this.onFilesAssociationChange()));
this.toDispose.push(this.onDidStateChange(e => this.onStateChange(e)));
this._register(this.fileService.onFileChanges(e => this.onFileChanges(e)));
this._register(this.textFileService.onAutoSaveConfigurationChange(config => this.updateAutoSaveConfiguration(config)));
this._register(this.textFileService.onFilesAssociationChange(e => this.onFilesAssociationChange()));
this._register(this.onDidStateChange(e => this.onStateChange(e)));
}
private onStateChange(e: StateChange): void {
@@ -183,71 +186,34 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
}
private updateAutoSaveConfiguration(config: IAutoSaveConfiguration): void {
if (typeof config.autoSaveDelay === 'number' && config.autoSaveDelay > 0) {
this.autoSaveAfterMillies = config.autoSaveDelay;
this.autoSaveAfterMilliesEnabled = true;
} else {
this.autoSaveAfterMillies = void 0;
this.autoSaveAfterMilliesEnabled = false;
}
const autoSaveAfterMilliesEnabled = (typeof config.autoSaveDelay === 'number') && config.autoSaveDelay > 0;
this.autoSaveAfterMilliesEnabled = autoSaveAfterMilliesEnabled;
this.autoSaveAfterMillies = autoSaveAfterMilliesEnabled ? config.autoSaveDelay : void 0;
}
private onFilesAssociationChange(): void {
this.updateTextEditorModelMode();
}
private updateTextEditorModelMode(modeId?: string): void {
if (!this.textEditorModel) {
return;
}
const firstLineText = this.getFirstLineText(this.textEditorModel);
const mode = this.getOrCreateMode(this.modeService, modeId, firstLineText);
const mode = this.getOrCreateMode(this.modeService, void 0, firstLineText);
this.modelService.setMode(this.textEditorModel, mode);
}
public get onDidContentChange(): Event<StateChange> {
return this._onDidContentChange.event;
}
public get onDidStateChange(): Event<StateChange> {
return this._onDidStateChange.event;
}
/**
* The current version id of the model.
*/
public getVersionId(): number {
getVersionId(): number {
return this.versionId;
}
/**
* Set a save error handler to install code that executes when save errors occur.
*/
public static setSaveErrorHandler(handler: ISaveErrorHandler): void {
TextFileEditorModel.saveErrorHandler = handler;
}
/**
* Set a save participant handler to react on models getting saved.
*/
public static setSaveParticipant(handler: ISaveParticipant): void {
TextFileEditorModel.saveParticipant = handler;
}
/**
* Discards any local changes and replaces the model with the contents of the version on disk.
*
* @param if the parameter soft is true, will not attempt to load the contents from disk.
*/
public revert(soft?: boolean): TPromise<void> {
revert(soft?: boolean): TPromise<void> {
if (!this.isResolved()) {
return TPromise.wrap<void>(null);
}
// Cancel any running auto-save
this.cancelAutoSavePromise();
this.cancelPendingAutoSave();
// Unset flags
const undo = this.setDirty(false);
@@ -272,28 +238,28 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
});
}
public load(options?: ILoadOptions): TPromise<TextFileEditorModel> {
diag('load() - enter', this.resource, new Date());
load(options?: ILoadOptions): TPromise<TextFileEditorModel> {
this.logService.trace('load() - enter', this.resource);
// It is very important to not reload the model when the model is dirty. We only want to reload the model from the disk
// if no save is pending to avoid data loss. This might cause a save conflict in case the file has been modified on the disk
// meanwhile, but this is a very low risk.
// It is very important to not reload the model when the model is dirty.
// We also only want to reload the model from the disk if no save is pending
// to avoid data loss.
if (this.dirty || this.saveSequentializer.hasPendingSave()) {
diag('load() - exit - without loading because model is dirty or being saved', this.resource, new Date());
this.logService.trace('load() - exit - without loading because model is dirty or being saved', this.resource);
return TPromise.as(this);
}
// Only for new models we support to load from backup
if (!this.textEditorModel && !this.createTextEditorModelPromise) {
return this.loadWithBackup(options);
return this.loadFromBackup(options);
}
// Otherwise load from file resource
return this.loadFromFile(options);
}
private loadWithBackup(options?: ILoadOptions): TPromise<TextFileEditorModel> {
private loadFromBackup(options?: ILoadOptions): TPromise<TextFileEditorModel> {
return this.backupFileService.loadBackupResource(this.resource).then(backup => {
// Make sure meanwhile someone else did not suceed or start loading
@@ -309,10 +275,11 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
mtime: Date.now(),
etag: void 0,
value: createTextBufferFactory(''), /* will be filled later from backup */
encoding: this.fileService.encoding.getWriteEncoding(this.resource, this.preferredEncoding)
encoding: this.fileService.encoding.getWriteEncoding(this.resource, this.preferredEncoding),
isReadonly: false
};
return this.loadWithContent(content, backup);
return this.loadWithContent(content, options, backup);
}
// Otherwise load from file
@@ -332,62 +299,77 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
etag = this.lastResolvedDiskStat.etag; // otherwise respect etag to support caching
}
// Ensure to track the versionId before doing a long running operation
// to make sure the model was not changed in the meantime which would
// indicate that the user or program has made edits. If we would ignore
// this, we could potentially loose the changes that were made because
// after resolving the content we update the model and reset the dirty
// flag.
const currentVersionId = this.versionId;
// Resolve Content
return this.textFileService
.resolveTextContent(this.resource, { acceptTextOnly: !allowBinary, etag, encoding: this.preferredEncoding })
.then(content => this.handleLoadSuccess(content), error => this.handleLoadError(error));
.then(content => {
// Clear orphaned state when loading was successful
this.setOrphaned(false);
// Guard against the model having changed in the meantime
if (currentVersionId === this.versionId) {
return this.loadWithContent(content, options);
}
return this;
}, error => {
const result = error.fileOperationResult;
// Apply orphaned state based on error code
this.setOrphaned(result === FileOperationResult.FILE_NOT_FOUND);
// NotModified status is expected and can be handled gracefully
if (result === FileOperationResult.FILE_NOT_MODIFIED_SINCE) {
// Guard against the model having changed in the meantime
if (currentVersionId === this.versionId) {
this.setDirty(false); // Ensure we are not tracking a stale state
}
return TPromise.as<TextFileEditorModel>(this);
}
// Ignore when a model has been resolved once and the file was deleted meanwhile. Since
// we already have the model loaded, we can return to this state and update the orphaned
// flag to indicate that this model has no version on disk anymore.
if (this.isResolved() && result === FileOperationResult.FILE_NOT_FOUND) {
return TPromise.as<TextFileEditorModel>(this);
}
// Otherwise bubble up the error
return TPromise.wrapError<TextFileEditorModel>(error);
});
}
private handleLoadSuccess(content: IRawTextContent): TPromise<TextFileEditorModel> {
// Clear orphaned state when load was successful
this.setOrphaned(false);
return this.loadWithContent(content);
}
private handleLoadError(error: FileOperationError): TPromise<TextFileEditorModel> {
const result = error.fileOperationResult;
// Apply orphaned state based on error code
this.setOrphaned(result === FileOperationResult.FILE_NOT_FOUND);
// NotModified status is expected and can be handled gracefully
if (result === FileOperationResult.FILE_NOT_MODIFIED_SINCE) {
this.setDirty(false); // Ensure we are not tracking a stale state
return TPromise.as<TextFileEditorModel>(this);
}
// Ignore when a model has been resolved once and the file was deleted meanwhile. Since
// we already have the model loaded, we can return to this state and update the orphaned
// flag to indicate that this model has no version on disk anymore.
if (this.isResolved() && result === FileOperationResult.FILE_NOT_FOUND) {
return TPromise.as<TextFileEditorModel>(this);
}
// Otherwise bubble up the error
return TPromise.wrapError<TextFileEditorModel>(error);
}
private loadWithContent(content: IRawTextContent, backup?: URI): TPromise<TextFileEditorModel> {
private loadWithContent(content: IRawTextContent, options?: ILoadOptions, backup?: URI): TPromise<TextFileEditorModel> {
return this.doLoadWithContent(content, backup).then(model => {
// Telemetry: We log the fileGet telemetry event after the model has been loaded to ensure a good mimetype
if (this.isSettingsFile()) {
const settingsType = this.getTypeIfSettings();
if (settingsType) {
/* __GDPR__
"settingsRead" : {}
"settingsRead" : {
"settingsType": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
}
*/
this.telemetryService.publicLog('settingsRead'); // Do not log read to user settings.json and .vscode folder as a fileGet event as it ruins our JSON usage data
this.telemetryService.publicLog('settingsRead', { settingsType }); // Do not log read to user settings.json and .vscode folder as a fileGet event as it ruins our JSON usage data
} else {
/* __GDPR__
"fileGet" : {
"mimeType" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"ext": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"path": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
"${include}": [
"${FileTelemetryData}"
]
}
*/
this.telemetryService.publicLog('fileGet', { mimeType: guessMimeTypes(this.resource.fsPath).join(', '), ext: path.extname(this.resource.fsPath), path: this.hashService.createSHA1(this.resource.fsPath) });
this.telemetryService.publicLog('fileGet', this.getTelemetryData(options && options.reason ? options.reason : LoadReason.OTHER));
}
return model;
@@ -395,19 +377,19 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
}
private doLoadWithContent(content: IRawTextContent, backup?: URI): TPromise<TextFileEditorModel> {
diag('load() - resolved content', this.resource, new Date());
this.logService.trace('load() - resolved content', this.resource);
// Update our resolved disk stat model
const resolvedStat: IFileStat = {
this.updateLastResolvedDiskStat({
resource: this.resource,
name: content.name,
mtime: content.mtime,
etag: content.etag,
isDirectory: false,
isSymbolicLink: false,
children: void 0
};
this.updateLastResolvedDiskStat(resolvedStat);
children: void 0,
isReadonly: content.isReadonly
} as IFileStat);
// Keep the original encoding to not loose it when saving
const oldEncoding = this.contentEncoding;
@@ -427,7 +409,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
// Join an existing request to create the editor model to avoid race conditions
else if (this.createTextEditorModelPromise) {
diag('load() - join existing text editor model promise', this.resource, new Date());
this.logService.trace('load() - join existing text editor model promise', this.resource);
return this.createTextEditorModelPromise;
}
@@ -437,7 +419,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
}
private doUpdateTextModel(value: ITextBufferFactory): TPromise<TextFileEditorModel> {
diag('load() - updated text editor model', this.resource, new Date());
this.logService.trace('load() - updated text editor model', this.resource);
// Ensure we are not tracking a stale state
this.setDirty(false);
@@ -457,7 +439,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
}
private doCreateTextModel(resource: URI, value: ITextBufferFactory, backup: URI): TPromise<TextFileEditorModel> {
diag('load() - created text editor model', this.resource, new Date());
this.logService.trace('load() - created text editor model', this.resource);
this.createTextEditorModelPromise = this.doLoadBackup(backup).then(backupContent => {
const hasBackupContent = !!backupContent;
@@ -501,7 +483,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
// where `value` was captured in the content change listener closure scope.
// Content Change
this.toDispose.push(this.textEditorModel.onDidChangeContent(() => this.onModelContentChanged()));
this._register(this.textEditorModel.onDidChangeContent(() => this.onModelContentChanged()));
}
private doLoadBackup(backup: URI): TPromise<ITextBufferFactory> {
@@ -517,11 +499,11 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
}
private onModelContentChanged(): void {
diag(`onModelContentChanged() - enter`, this.resource, new Date());
this.logService.trace(`onModelContentChanged() - enter`, this.resource);
// In any case increment the version id because it tracks the textual content state of the model at all times
this.versionId++;
diag(`onModelContentChanged() - new versionId ${this.versionId}`, this.resource, new Date());
this.logService.trace(`onModelContentChanged() - new versionId ${this.versionId}`, this.resource);
// Ignore if blocking model changes
if (this.blockModelContentChange) {
@@ -533,7 +515,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
// Note: we currently only do this check when auto-save is turned off because there you see
// a dirty indicator that you want to get rid of when undoing to the saved version.
if (!this.autoSaveAfterMilliesEnabled && this.textEditorModel.getAlternativeVersionId() === this.bufferSavedVersionId) {
diag('onModelContentChanged() - model content changed back to last saved version', this.resource, new Date());
this.logService.trace('onModelContentChanged() - model content changed back to last saved version', this.resource);
// Clear flags
const wasDirty = this.dirty;
@@ -547,7 +529,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
return;
}
diag('onModelContentChanged() - model content changed and marked as dirty', this.resource, new Date());
this.logService.trace('onModelContentChanged() - model content changed and marked as dirty', this.resource);
// Mark as dirty
this.makeDirty();
@@ -557,7 +539,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
if (!this.inConflictMode) {
this.doAutoSave(this.versionId);
} else {
diag('makeDirty() - prevented save because we are in conflict resolution mode', this.resource, new Date());
this.logService.trace('makeDirty() - prevented save because we are in conflict resolution mode', this.resource);
}
}
@@ -577,53 +559,50 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
}
}
private doAutoSave(versionId: number): TPromise<void> {
diag(`doAutoSave() - enter for versionId ${versionId}`, this.resource, new Date());
private doAutoSave(versionId: number): void {
this.logService.trace(`doAutoSave() - enter for versionId ${versionId}`, this.resource);
// Cancel any currently running auto saves to make this the one that succeeds
this.cancelAutoSavePromise();
this.cancelPendingAutoSave();
// Create new save promise and keep it
this.autoSavePromise = TPromise.timeout(this.autoSaveAfterMillies).then(() => {
// Create new save timer and store it for disposal as needed
const handle = setTimeout(() => {
// Only trigger save if the version id has not changed meanwhile
if (versionId === this.versionId) {
this.doSave(versionId, { reason: SaveReason.AUTO }).done(null, onUnexpectedError); // Very important here to not return the promise because if the timeout promise is canceled it will bubble up the error otherwise - do not change
}
});
}, this.autoSaveAfterMillies);
return this.autoSavePromise;
this.autoSaveDisposable = toDisposable(() => clearTimeout(handle));
}
private cancelAutoSavePromise(): void {
if (this.autoSavePromise) {
this.autoSavePromise.cancel();
this.autoSavePromise = void 0;
private cancelPendingAutoSave(): void {
if (this.autoSaveDisposable) {
this.autoSaveDisposable.dispose();
this.autoSaveDisposable = void 0;
}
}
/**
* Saves the current versionId of this editor model if it is dirty.
*/
public save(options: ISaveOptions = Object.create(null)): TPromise<void> {
save(options: ISaveOptions = Object.create(null)): TPromise<void> {
if (!this.isResolved()) {
return TPromise.wrap<void>(null);
}
diag('save() - enter', this.resource, new Date());
this.logService.trace('save() - enter', this.resource);
// Cancel any currently running auto saves to make this the one that succeeds
this.cancelAutoSavePromise();
this.cancelPendingAutoSave();
return this.doSave(this.versionId, options);
}
private doSave(versionId: number, options: ISaveOptions): TPromise<void> {
if (types.isUndefinedOrNull(options.reason)) {
if (isUndefinedOrNull(options.reason)) {
options.reason = SaveReason.EXPLICIT;
}
diag(`doSave(${versionId}) - enter with versionId ' + versionId`, this.resource, new Date());
this.logService.trace(`doSave(${versionId}) - enter with versionId ' + versionId`, this.resource);
// Lookup any running pending save for this versionId and return it if found
//
@@ -631,7 +610,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
// while the save was not yet finished to disk
//
if (this.saveSequentializer.hasPendingSave(versionId)) {
diag(`doSave(${versionId}) - exit - found a pending save for versionId ${versionId}`, this.resource, new Date());
this.logService.trace(`doSave(${versionId}) - exit - found a pending save for versionId ${versionId}`, this.resource);
return this.saveSequentializer.pendingSave;
}
@@ -644,7 +623,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
// Thus we avoid spawning multiple auto saves and only take the latest.
//
if ((!options.force && !this.dirty) || versionId !== this.versionId) {
diag(`doSave(${versionId}) - exit - because not dirty and/or versionId is different (this.isDirty: ${this.dirty}, this.versionId: ${this.versionId})`, this.resource, new Date());
this.logService.trace(`doSave(${versionId}) - exit - because not dirty and/or versionId is different (this.isDirty: ${this.dirty}, this.versionId: ${this.versionId})`, this.resource);
return TPromise.wrap<void>(null);
}
@@ -658,7 +637,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
// while the first save has not returned yet.
//
if (this.saveSequentializer.hasPendingSave()) {
diag(`doSave(${versionId}) - exit - because busy saving`, this.resource, new Date());
this.logService.trace(`doSave(${versionId}) - exit - because busy saving`, this.resource);
// Register this as the next upcoming save and return
return this.saveSequentializer.setNext(() => this.doSave(this.versionId /* make sure to use latest version id here */, options));
@@ -692,6 +671,16 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
// mark the save participant as current pending save operation
return this.saveSequentializer.setPending(versionId, saveParticipantPromise.then(newVersionId => {
// We have to protect against being disposed at this point. It could be that the save() operation
// was triggerd followed by a dispose() operation right after without waiting. Typically we cannot
// be disposed if we are dirty, but if we are not dirty, save() and dispose() can still be triggered
// one after the other without waiting for the save() to complete. If we are disposed(), we risk
// saving contents to disk that are stale (see https://github.com/Microsoft/vscode/issues/50942).
// To fix this issue, we will not store the contents to disk when we got disposed.
if (this.disposed) {
return void 0;
}
// Under certain conditions we do a short-cut of flushing contents to disk when we can assume that
// the file has not changed and as such was not dirty before.
// The conditions are all of:
@@ -714,7 +703,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
// Save to Disk
// mark the save operation as currently pending with the versionId (it might have changed from a save participant triggering)
diag(`doSave(${versionId}) - before updateContent()`, this.resource, new Date());
this.logService.trace(`doSave(${versionId}) - before updateContent()`, this.resource);
return this.saveSequentializer.setPending(newVersionId, this.fileService.updateContent(this.lastResolvedDiskStat.resource, this.createSnapshot(), {
overwriteReadonly: options.overwriteReadonly,
overwriteEncoding: options.overwriteEncoding,
@@ -723,30 +712,34 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
etag: this.lastResolvedDiskStat.etag,
writeElevated: options.writeElevated
}).then(stat => {
diag(`doSave(${versionId}) - after updateContent()`, this.resource, new Date());
this.logService.trace(`doSave(${versionId}) - after updateContent()`, this.resource);
// Telemetry
if (this.isSettingsFile()) {
const settingsType = this.getTypeIfSettings();
if (settingsType) {
/* __GDPR__
"settingsWritten" : {}
"settingsWritten" : {
"settingsType": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
}
*/
this.telemetryService.publicLog('settingsWritten'); // Do not log write to user settings.json and .vscode folder as a filePUT event as it ruins our JSON usage data
this.telemetryService.publicLog('settingsWritten', { settingsType }); // Do not log write to user settings.json and .vscode folder as a filePUT event as it ruins our JSON usage data
} else {
/* __GDPR__
"filePUT" : {
"mimeType" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"ext": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
"${include}": [
"${FileTelemetryData}"
]
}
*/
this.telemetryService.publicLog('filePUT', { mimeType: guessMimeTypes(this.resource.fsPath).join(', '), ext: path.extname(this.lastResolvedDiskStat.resource.fsPath) });
this.telemetryService.publicLog('filePUT', this.getTelemetryData(options.reason));
}
// Update dirty state unless model has changed meanwhile
if (versionId === this.versionId) {
diag(`doSave(${versionId}) - setting dirty to false because versionId did not change`, this.resource, new Date());
this.logService.trace(`doSave(${versionId}) - setting dirty to false because versionId did not change`, this.resource);
this.setDirty(false);
} else {
diag(`doSave(${versionId}) - not setting dirty to false because versionId did change meanwhile`, this.resource, new Date());
this.logService.trace(`doSave(${versionId}) - not setting dirty to false because versionId did change meanwhile`, this.resource);
}
// Updated resolved stat with updated stat
@@ -758,7 +751,11 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
// Emit File Saved Event
this._onDidStateChange.fire(StateChange.SAVED);
}, error => {
diag(`doSave(${versionId}) - exit - resulted in a save error: ${error.toString()}`, this.resource, new Date());
if (!error) {
error = new Error('Unknown Save Error'); // TODO@remote we should never get null as error (https://github.com/Microsoft/vscode/issues/55051)
}
this.logService.error(`doSave(${versionId}) - exit - resulted in a save error: ${error.toString()}`, this.resource);
// Flag as error state in the model
this.inErrorMode = true;
@@ -777,18 +774,71 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
}));
}
private isSettingsFile(): boolean {
private getTypeIfSettings(): string {
if (path.extname(this.resource.fsPath) !== '.json') {
return '';
}
// Check for global settings file
if (this.resource.fsPath === this.environmentService.appSettingsPath) {
return true;
if (isEqual(this.resource, URI.file(this.environmentService.appSettingsPath), !isLinux)) {
return 'global-settings';
}
// Check for keybindings file
if (isEqual(this.resource, URI.file(this.environmentService.appKeybindingsPath), !isLinux)) {
return 'keybindings';
}
// Check for locale file
if (isEqual(this.resource, URI.file(path.join(this.environmentService.appSettingsHome, 'locale.json')), !isLinux)) {
return 'locale';
}
// Check for snippets
if (isEqualOrParent(this.resource, URI.file(path.join(this.environmentService.appSettingsHome, 'snippets')), hasToIgnoreCase(this.resource))) {
return 'snippets';
}
// Check for workspace settings file
return this.contextService.getWorkspace().folders.some(folder => {
const folders = this.contextService.getWorkspace().folders;
for (let i = 0; i < folders.length; i++) {
// {{SQL CARBON EDIT}}
return path.isEqualOrParent(this.resource.fsPath, path.join(folder.uri.fsPath, '.sqlops'));
});
if (isEqualOrParent(this.resource, folders[i].toResource('.sqlops'), hasToIgnoreCase(this.resource))) {
const filename = path.basename(this.resource.fsPath);
if (TextFileEditorModel.WHITELIST_WORKSPACE_JSON.indexOf(filename) > -1) {
// {{SQL CARBON EDIT}}
return `.sqlops/${filename}`;
}
}
}
return '';
}
private getTelemetryData(reason: number): Object {
const ext = path.extname(this.resource.fsPath);
const fileName = path.basename(this.resource.fsPath);
const telemetryData = {
mimeType: guessMimeTypes(this.resource.fsPath).join(', '),
ext,
path: this.hashService.createSHA1(this.resource.fsPath),
reason
};
if (ext === '.json' && TextFileEditorModel.WHITELIST_JSON.indexOf(fileName) > -1) {
telemetryData['whitelistedjson'] = fileName;
}
/* __GDPR__FRAGMENT__
"FileTelemetryData" : {
"mimeType" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"ext": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"path": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"reason": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
"whitelistedjson": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
}
*/
return telemetryData;
}
private doTouch(versionId: number): TPromise<void> {
@@ -864,31 +914,19 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
TextFileEditorModel.saveErrorHandler.onSaveError(error, this);
}
/**
* Returns true if the content of this model has changes that are not yet saved back to the disk.
*/
public isDirty(): boolean {
isDirty(): boolean {
return this.dirty;
}
/**
* Returns the time in millies when this working copy was attempted to be saved.
*/
public getLastSaveAttemptTime(): number {
getLastSaveAttemptTime(): number {
return this.lastSaveAttemptTime;
}
/**
* Returns the time in millies when this working copy was last modified by the user or some other program.
*/
public getETag(): string {
getETag(): string {
return this.lastResolvedDiskStat ? this.lastResolvedDiskStat.etag : null;
}
/**
* Answers if this model is in a specific state.
*/
public hasState(state: ModelState): boolean {
hasState(state: ModelState): boolean {
switch (state) {
case ModelState.CONFLICT:
return this.inConflictMode;
@@ -905,11 +943,11 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
}
}
public getEncoding(): string {
getEncoding(): string {
return this.preferredEncoding || this.contentEncoding;
}
public setEncoding(encoding: string, mode: EncodingMode): void {
setEncoding(encoding: string, mode: EncodingMode): void {
if (!this.isNewEncoding(encoding)) {
return; // return early if the encoding is already the same
}
@@ -946,7 +984,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
}
}
public updatePreferredEncoding(encoding: string): void {
updatePreferredEncoding(encoding: string): void {
if (!this.isNewEncoding(encoding)) {
return;
}
@@ -969,41 +1007,35 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
return true;
}
public isResolved(): boolean {
return !types.isUndefinedOrNull(this.lastResolvedDiskStat);
isResolved(): boolean {
return !isUndefinedOrNull(this.lastResolvedDiskStat);
}
/**
* Returns true if the dispose() method of this model has been called.
*/
public isDisposed(): boolean {
isReadonly(): boolean {
return this.lastResolvedDiskStat && this.lastResolvedDiskStat.isReadonly;
}
isDisposed(): boolean {
return this.disposed;
}
/**
* Returns the full resource URI of the file this text file editor model is about.
*/
public getResource(): URI {
getResource(): URI {
return this.resource;
}
/**
* Stat accessor only used by tests.
*/
public getStat(): IFileStat {
getStat(): IFileStat {
return this.lastResolvedDiskStat;
}
public dispose(): void {
dispose(): void {
this.disposed = true;
this.inConflictMode = false;
this.inOrphanMode = false;
this.inErrorMode = false;
this.toDispose = dispose(this.toDispose);
this.createTextEditorModelPromise = null;
this.cancelAutoSavePromise();
this.cancelPendingAutoSave();
super.dispose();
}
@@ -1025,7 +1057,7 @@ export class SaveSequentializer {
private _pendingSave: IPendingSave;
private _nextSave: ISaveOperation;
public hasPendingSave(versionId?: number): boolean {
hasPendingSave(versionId?: number): boolean {
if (!this._pendingSave) {
return false;
}
@@ -1037,11 +1069,11 @@ export class SaveSequentializer {
return !!this._pendingSave;
}
public get pendingSave(): TPromise<void> {
get pendingSave(): TPromise<void> {
return this._pendingSave ? this._pendingSave.promise : void 0;
}
public setPending(versionId: number, promise: TPromise<void>): TPromise<void> {
setPending(versionId: number, promise: TPromise<void>): TPromise<void> {
this._pendingSave = { versionId, promise };
promise.done(() => this.donePending(versionId), () => this.donePending(versionId));
@@ -1070,7 +1102,7 @@ export class SaveSequentializer {
}
}
public setNext(run: () => TPromise<void>): TPromise<void> {
setNext(run: () => TPromise<void>): TPromise<void> {
// this is our first next save, so we create associated promise with it
// so that we can return a promise that completes when the save operation
@@ -1104,15 +1136,7 @@ class DefaultSaveErrorHandler implements ISaveErrorHandler {
constructor(@INotificationService private notificationService: INotificationService) { }
public onSaveError(error: any, model: TextFileEditorModel): void {
onSaveError(error: any, model: TextFileEditorModel): void {
this.notificationService.error(nls.localize('genericSaveError', "Failed to save '{0}': {1}", path.basename(model.getResource().fsPath), toErrorMessage(error, false)));
}
}
// Diagnostics support
let diag: (...args: any[]) => void;
if (!diag) {
diag = diagnostics.register('TextFileEditorModelDiagnostics', function (...args: any[]) {
console.log(args[1] + ' - ' + args[0] + ' (time: ' + args[2].getTime() + ' [' + args[2].toUTCString() + '])');
});
}

View File

@@ -8,23 +8,38 @@ import { Event, Emitter, debounceEvent } from 'vs/base/common/event';
import { TPromise } from 'vs/base/common/winjs.base';
import URI from 'vs/base/common/uri';
import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel';
import { dispose, IDisposable } from 'vs/base/common/lifecycle';
import { ITextFileEditorModel, ITextFileEditorModelManager, TextFileModelChangeEvent, StateChange, IModelLoadOrCreateOptions, ILoadOptions } from 'vs/workbench/services/textfile/common/textfiles';
import { dispose, IDisposable, Disposable } from 'vs/base/common/lifecycle';
import { ITextFileEditorModel, ITextFileEditorModelManager, TextFileModelChangeEvent, StateChange, IModelLoadOrCreateOptions } from 'vs/workbench/services/textfile/common/textfiles';
import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ResourceMap } from 'vs/base/common/map';
import { onUnexpectedError } from 'vs/base/common/errors';
export class TextFileEditorModelManager implements ITextFileEditorModelManager {
private toUnbind: IDisposable[];
export class TextFileEditorModelManager extends Disposable implements ITextFileEditorModelManager {
private readonly _onModelDisposed: Emitter<URI>;
private readonly _onModelContentChanged: Emitter<TextFileModelChangeEvent>;
private readonly _onModelDirty: Emitter<TextFileModelChangeEvent>;
private readonly _onModelSaveError: Emitter<TextFileModelChangeEvent>;
private readonly _onModelSaved: Emitter<TextFileModelChangeEvent>;
private readonly _onModelReverted: Emitter<TextFileModelChangeEvent>;
private readonly _onModelEncodingChanged: Emitter<TextFileModelChangeEvent>;
private readonly _onModelOrphanedChanged: Emitter<TextFileModelChangeEvent>;
private readonly _onModelDisposed: Emitter<URI> = this._register(new Emitter<URI>());
get onModelDisposed(): Event<URI> { return this._onModelDisposed.event; }
private readonly _onModelContentChanged: Emitter<TextFileModelChangeEvent> = this._register(new Emitter<TextFileModelChangeEvent>());
get onModelContentChanged(): Event<TextFileModelChangeEvent> { return this._onModelContentChanged.event; }
private readonly _onModelDirty: Emitter<TextFileModelChangeEvent> = this._register(new Emitter<TextFileModelChangeEvent>());
get onModelDirty(): Event<TextFileModelChangeEvent> { return this._onModelDirty.event; }
private readonly _onModelSaveError: Emitter<TextFileModelChangeEvent> = this._register(new Emitter<TextFileModelChangeEvent>());
get onModelSaveError(): Event<TextFileModelChangeEvent> { return this._onModelSaveError.event; }
private readonly _onModelSaved: Emitter<TextFileModelChangeEvent> = this._register(new Emitter<TextFileModelChangeEvent>());
get onModelSaved(): Event<TextFileModelChangeEvent> { return this._onModelSaved.event; }
private readonly _onModelReverted: Emitter<TextFileModelChangeEvent> = this._register(new Emitter<TextFileModelChangeEvent>());
get onModelReverted(): Event<TextFileModelChangeEvent> { return this._onModelReverted.event; }
private readonly _onModelEncodingChanged: Emitter<TextFileModelChangeEvent> = this._register(new Emitter<TextFileModelChangeEvent>());
get onModelEncodingChanged(): Event<TextFileModelChangeEvent> { return this._onModelEncodingChanged.event; }
private readonly _onModelOrphanedChanged: Emitter<TextFileModelChangeEvent> = this._register(new Emitter<TextFileModelChangeEvent>());
get onModelOrphanedChanged(): Event<TextFileModelChangeEvent> { return this._onModelOrphanedChanged.event; }
private _onModelsDirtyEvent: Event<TextFileModelChangeEvent[]>;
private _onModelsSaveError: Event<TextFileModelChangeEvent[]>;
@@ -41,25 +56,7 @@ export class TextFileEditorModelManager implements ITextFileEditorModelManager {
@ILifecycleService private lifecycleService: ILifecycleService,
@IInstantiationService private instantiationService: IInstantiationService
) {
this.toUnbind = [];
this._onModelDisposed = new Emitter<URI>();
this._onModelContentChanged = new Emitter<TextFileModelChangeEvent>();
this._onModelDirty = new Emitter<TextFileModelChangeEvent>();
this._onModelSaveError = new Emitter<TextFileModelChangeEvent>();
this._onModelSaved = new Emitter<TextFileModelChangeEvent>();
this._onModelReverted = new Emitter<TextFileModelChangeEvent>();
this._onModelEncodingChanged = new Emitter<TextFileModelChangeEvent>();
this._onModelOrphanedChanged = new Emitter<TextFileModelChangeEvent>();
this.toUnbind.push(this._onModelDisposed);
this.toUnbind.push(this._onModelContentChanged);
this.toUnbind.push(this._onModelDirty);
this.toUnbind.push(this._onModelSaveError);
this.toUnbind.push(this._onModelSaved);
this.toUnbind.push(this._onModelReverted);
this.toUnbind.push(this._onModelEncodingChanged);
this.toUnbind.push(this._onModelOrphanedChanged);
super();
this.mapResourceToModel = new ResourceMap<ITextFileEditorModel>();
this.mapResourceToDisposeListener = new ResourceMap<IDisposable>();
@@ -76,39 +73,7 @@ export class TextFileEditorModelManager implements ITextFileEditorModelManager {
this.lifecycleService.onShutdown(this.dispose, this);
}
public get onModelDisposed(): Event<URI> {
return this._onModelDisposed.event;
}
public get onModelContentChanged(): Event<TextFileModelChangeEvent> {
return this._onModelContentChanged.event;
}
public get onModelDirty(): Event<TextFileModelChangeEvent> {
return this._onModelDirty.event;
}
public get onModelSaveError(): Event<TextFileModelChangeEvent> {
return this._onModelSaveError.event;
}
public get onModelSaved(): Event<TextFileModelChangeEvent> {
return this._onModelSaved.event;
}
public get onModelReverted(): Event<TextFileModelChangeEvent> {
return this._onModelReverted.event;
}
public get onModelEncodingChanged(): Event<TextFileModelChangeEvent> {
return this._onModelEncodingChanged.event;
}
public get onModelOrphanedChanged(): Event<TextFileModelChangeEvent> {
return this._onModelOrphanedChanged.event;
}
public get onModelsDirty(): Event<TextFileModelChangeEvent[]> {
get onModelsDirty(): Event<TextFileModelChangeEvent[]> {
if (!this._onModelsDirtyEvent) {
this._onModelsDirtyEvent = this.debounce(this.onModelDirty);
}
@@ -116,7 +81,7 @@ export class TextFileEditorModelManager implements ITextFileEditorModelManager {
return this._onModelsDirtyEvent;
}
public get onModelsSaveError(): Event<TextFileModelChangeEvent[]> {
get onModelsSaveError(): Event<TextFileModelChangeEvent[]> {
if (!this._onModelsSaveError) {
this._onModelsSaveError = this.debounce(this.onModelSaveError);
}
@@ -124,7 +89,7 @@ export class TextFileEditorModelManager implements ITextFileEditorModelManager {
return this._onModelsSaveError;
}
public get onModelsSaved(): Event<TextFileModelChangeEvent[]> {
get onModelsSaved(): Event<TextFileModelChangeEvent[]> {
if (!this._onModelsSaved) {
this._onModelsSaved = this.debounce(this.onModelSaved);
}
@@ -132,7 +97,7 @@ export class TextFileEditorModelManager implements ITextFileEditorModelManager {
return this._onModelsSaved;
}
public get onModelsReverted(): Event<TextFileModelChangeEvent[]> {
get onModelsReverted(): Event<TextFileModelChangeEvent[]> {
if (!this._onModelsReverted) {
this._onModelsReverted = this.debounce(this.onModelReverted);
}
@@ -155,11 +120,11 @@ export class TextFileEditorModelManager implements ITextFileEditorModelManager {
return 250;
}
public get(resource: URI): ITextFileEditorModel {
get(resource: URI): ITextFileEditorModel {
return this.mapResourceToModel.get(resource);
}
public loadOrCreate(resource: URI, options?: IModelLoadOrCreateOptions): TPromise<ITextFileEditorModel> {
loadOrCreate(resource: URI, options?: IModelLoadOrCreateOptions): TPromise<ITextFileEditorModel> {
// Return early if model is currently being loaded
const pendingLoad = this.mapResourceToPendingModelLoaders.get(resource);
@@ -167,18 +132,23 @@ export class TextFileEditorModelManager implements ITextFileEditorModelManager {
return pendingLoad;
}
let modelLoadOptions: ILoadOptions;
if (options && options.allowBinary) {
modelLoadOptions = { allowBinary: true };
}
let modelPromise: TPromise<ITextFileEditorModel>;
// Model exists
let model = this.get(resource);
if (model) {
if (options && options.reload) {
modelPromise = model.load(modelLoadOptions);
// async reload: trigger a reload but return immediately
if (options.reload.async) {
modelPromise = TPromise.as(model);
model.load(options).then(null, onUnexpectedError);
}
// sync reload: do not return until model reloaded
else {
modelPromise = model.load(options);
}
} else {
modelPromise = TPromise.as(model);
}
@@ -187,7 +157,7 @@ export class TextFileEditorModelManager implements ITextFileEditorModelManager {
// Model does not exist
else {
model = this.instantiationService.createInstance(TextFileEditorModel, resource, options ? options.encoding : void 0);
modelPromise = model.load(modelLoadOptions);
modelPromise = model.load(options);
// Install state change listener
this.mapResourceToStateChangeListener.set(resource, model.onDidStateChange(state => {
@@ -249,7 +219,7 @@ export class TextFileEditorModelManager implements ITextFileEditorModelManager {
});
}
public getAll(resource?: URI, filter?: (model: ITextFileEditorModel) => boolean): ITextFileEditorModel[] {
getAll(resource?: URI, filter?: (model: ITextFileEditorModel) => boolean): ITextFileEditorModel[] {
if (resource) {
const res = this.mapResourceToModel.get(resource);
@@ -266,7 +236,7 @@ export class TextFileEditorModelManager implements ITextFileEditorModelManager {
return res;
}
public add(resource: URI, model: ITextFileEditorModel): void {
add(resource: URI, model: ITextFileEditorModel): void {
const knownModel = this.mapResourceToModel.get(resource);
if (knownModel === model) {
return; // already cached
@@ -286,7 +256,7 @@ export class TextFileEditorModelManager implements ITextFileEditorModelManager {
}));
}
public remove(resource: URI): void {
remove(resource: URI): void {
this.mapResourceToModel.delete(resource);
const disposeListener = this.mapResourceToDisposeListener.get(resource);
@@ -308,7 +278,7 @@ export class TextFileEditorModelManager implements ITextFileEditorModelManager {
}
}
public clear(): void {
clear(): void {
// model caches
this.mapResourceToModel.clear();
@@ -327,7 +297,7 @@ export class TextFileEditorModelManager implements ITextFileEditorModelManager {
this.mapResourceToModelContentChangeListener.clear();
}
public disposeModel(model: TextFileEditorModel): void {
disposeModel(model: TextFileEditorModel): void {
if (!model) {
return; // we need data!
}
@@ -346,8 +316,4 @@ export class TextFileEditorModelManager implements ITextFileEditorModelManager {
model.dispose();
}
public dispose(): void {
this.toUnbind = dispose(this.toUnbind);
}
}

View File

@@ -14,13 +14,13 @@ import { Event, Emitter } from 'vs/base/common/event';
import * as platform from 'vs/base/common/platform';
import { IWindowsService } from 'vs/platform/windows/common/windows';
import { IBackupFileService } from 'vs/workbench/services/backup/common/backup';
import { IResult, ITextFileOperationResult, ITextFileService, IRawTextContent, IAutoSaveConfiguration, AutoSaveMode, SaveReason, ITextFileEditorModelManager, ITextFileEditorModel, ModelState, ISaveOptions, AutoSaveContext } from 'vs/workbench/services/textfile/common/textfiles';
import { ConfirmResult } from 'vs/workbench/common/editor';
import { IResult, ITextFileOperationResult, ITextFileService, IRawTextContent, IAutoSaveConfiguration, AutoSaveMode, SaveReason, ITextFileEditorModelManager, ITextFileEditorModel, ModelState, ISaveOptions, AutoSaveContext, IWillMoveEvent } from 'vs/workbench/services/textfile/common/textfiles';
import { ConfirmResult, IRevertOptions } from 'vs/workbench/common/editor';
import { ILifecycleService, ShutdownReason } from 'vs/platform/lifecycle/common/lifecycle';
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
import { IFileService, IResolveContentOptions, IFilesConfiguration, FileOperationError, FileOperationResult, AutoSaveConfiguration, HotExitConfiguration } from 'vs/platform/files/common/files';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { Disposable } from 'vs/base/common/lifecycle';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService';
import { UntitledEditorModel } from 'vs/workbench/common/editor/untitledEditorModel';
@@ -29,11 +29,11 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti
import { ResourceMap } from 'vs/base/common/map';
import { Schemas } from 'vs/base/common/network';
import { IHistoryService } from 'vs/workbench/services/history/common/history';
import { IRevertOptions } from 'vs/platform/editor/common/editor';
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { createTextBufferFactoryFromSnapshot } from 'vs/editor/common/model/textModel';
import { IModelService } from 'vs/editor/common/services/modelService';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { isEqualOrParent, isEqual } from 'vs/base/common/resources';
export interface IBackupResult {
didBackup: boolean;
@@ -44,24 +44,26 @@ export interface IBackupResult {
*
* It also adds diagnostics and logging around file system operations.
*/
export abstract class TextFileService implements ITextFileService {
export abstract class TextFileService extends Disposable implements ITextFileService {
public _serviceBrand: any;
_serviceBrand: any;
private readonly _onFilesAssociationChange: Emitter<void> = this._register(new Emitter<void>());
get onAutoSaveConfigurationChange(): Event<IAutoSaveConfiguration> { return this._onAutoSaveConfigurationChange.event; }
private readonly _onAutoSaveConfigurationChange: Emitter<IAutoSaveConfiguration> = this._register(new Emitter<IAutoSaveConfiguration>());
get onFilesAssociationChange(): Event<void> { return this._onFilesAssociationChange.event; }
private readonly _onWillMove = this._register(new Emitter<IWillMoveEvent>());
get onWillMove(): Event<IWillMoveEvent> { return this._onWillMove.event; }
private toUnbind: IDisposable[];
private _models: TextFileEditorModelManager;
private readonly _onFilesAssociationChange: Emitter<void>;
private currentFilesAssociationConfig: { [key: string]: string; };
private readonly _onAutoSaveConfigurationChange: Emitter<IAutoSaveConfiguration>;
private configuredAutoSaveDelay: number;
private configuredAutoSaveOnFocusChange: boolean;
private configuredAutoSaveOnWindowChange: boolean;
private autoSaveContext: IContextKey<string>;
private configuredHotExit: string;
private autoSaveContext: IContextKey<string>;
constructor(
private lifecycleService: ILifecycleService,
@@ -78,13 +80,7 @@ export abstract class TextFileService implements ITextFileService {
contextKeyService: IContextKeyService,
private modelService: IModelService
) {
this.toUnbind = [];
this._onAutoSaveConfigurationChange = new Emitter<IAutoSaveConfiguration>();
this.toUnbind.push(this._onAutoSaveConfigurationChange);
this._onFilesAssociationChange = new Emitter<void>();
this.toUnbind.push(this._onFilesAssociationChange);
super();
this._models = this.instantiationService.createInstance(TextFileEditorModelManager);
this.autoSaveContext = AutoSaveContext.bindTo(contextKeyService);
@@ -97,7 +93,7 @@ export abstract class TextFileService implements ITextFileService {
this.registerListeners();
}
public get models(): ITextFileEditorModelManager {
get models(): ITextFileEditorModelManager {
return this._models;
}
@@ -107,14 +103,6 @@ export abstract class TextFileService implements ITextFileService {
abstract confirmSave(resources?: URI[]): TPromise<ConfirmResult>;
public get onAutoSaveConfigurationChange(): Event<IAutoSaveConfiguration> {
return this._onAutoSaveConfigurationChange.event;
}
public get onFilesAssociationChange(): Event<void> {
return this._onFilesAssociationChange.event;
}
private registerListeners(): void {
// Lifecycle
@@ -122,7 +110,7 @@ export abstract class TextFileService implements ITextFileService {
this.lifecycleService.onShutdown(this.dispose, this);
// Files configuration changes
this.toUnbind.push(this.configurationService.onDidChangeConfiguration(e => {
this._register(this.configurationService.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('files')) {
this.onFilesConfigurationChange(this.configurationService.getValue<IFilesConfiguration>());
}
@@ -365,7 +353,7 @@ export abstract class TextFileService implements ITextFileService {
}
}
public getDirty(resources?: URI[]): URI[] {
getDirty(resources?: URI[]): URI[] {
// Collect files
const dirty = this.getDirtyFileModels(resources).map(m => m.getResource());
@@ -376,7 +364,7 @@ export abstract class TextFileService implements ITextFileService {
return dirty;
}
public isDirty(resource?: URI): boolean {
isDirty(resource?: URI): boolean {
// Check for dirty file
if (this._models.getAll(resource).some(model => model.isDirty())) {
@@ -387,7 +375,7 @@ export abstract class TextFileService implements ITextFileService {
return this.untitledEditorService.getDirty().some(dirty => !resource || dirty.toString() === resource.toString());
}
public save(resource: URI, options?: ISaveOptions): TPromise<boolean> {
save(resource: URI, options?: ISaveOptions): TPromise<boolean> {
// Run a forced save if we detect the file is not dirty so that save participants can still run
if (options && options.force && this.fileService.canHandleResource(resource) && !this.isDirty(resource)) {
@@ -400,9 +388,9 @@ export abstract class TextFileService implements ITextFileService {
return this.saveAll([resource], options).then(result => result.results.length === 1 && result.results[0].success);
}
public saveAll(includeUntitled?: boolean, options?: ISaveOptions): TPromise<ITextFileOperationResult>;
public saveAll(resources: URI[], options?: ISaveOptions): TPromise<ITextFileOperationResult>;
public saveAll(arg1?: any, options?: ISaveOptions): TPromise<ITextFileOperationResult> {
saveAll(includeUntitled?: boolean, options?: ISaveOptions): TPromise<ITextFileOperationResult>;
saveAll(resources: URI[], options?: ISaveOptions): TPromise<ITextFileOperationResult>;
saveAll(arg1?: any, options?: ISaveOptions): TPromise<ITextFileOperationResult> {
// get all dirty
let toSave: URI[] = [];
@@ -436,16 +424,16 @@ export abstract class TextFileService implements ITextFileService {
for (let i = 0; i < untitledResources.length; i++) {
const untitled = untitledResources[i];
if (this.untitledEditorService.exists(untitled)) {
let targetPath: string;
let targetUri: URI;
// Untitled with associated file path don't need to prompt
if (this.untitledEditorService.hasAssociatedFilePath(untitled)) {
targetPath = untitled.fsPath;
targetUri = untitled.with({ scheme: Schemas.file });
}
// Otherwise ask user
else {
targetPath = await this.promptForPath(this.suggestFileName(untitled));
const targetPath = await this.promptForPath(this.suggestFileName(untitled));
if (!targetPath) {
return TPromise.as({
results: [...fileResources, ...untitledResources].map(r => {
@@ -455,9 +443,11 @@ export abstract class TextFileService implements ITextFileService {
})
});
}
targetUri = URI.file(targetPath);
}
targetsForUntitled.push(URI.file(targetPath));
targetsForUntitled.push(targetUri);
}
}
@@ -532,7 +522,7 @@ export abstract class TextFileService implements ITextFileService {
return this.getFileModels(arg1).filter(model => model.isDirty());
}
public saveAs(resource: URI, target?: URI, options?: ISaveOptions): TPromise<URI> {
saveAs(resource: URI, target?: URI, options?: ISaveOptions): TPromise<URI> {
// Get to target resource
let targetPromise: TPromise<URI>;
@@ -649,11 +639,11 @@ export abstract class TextFileService implements ITextFileService {
return untitledFileName;
}
public revert(resource: URI, options?: IRevertOptions): TPromise<boolean> {
revert(resource: URI, options?: IRevertOptions): TPromise<boolean> {
return this.revertAll([resource], options).then(result => result.results.length === 1 && result.results[0].success);
}
public revertAll(resources?: URI[], options?: IRevertOptions): TPromise<ITextFileOperationResult> {
revertAll(resources?: URI[], options?: IRevertOptions): TPromise<ITextFileOperationResult> {
// Revert files first
return this.doRevertAllFiles(resources, options).then(operation => {
@@ -702,7 +692,107 @@ export abstract class TextFileService implements ITextFileService {
});
}
public getAutoSaveMode(): AutoSaveMode {
create(resource: URI, contents?: string, options?: { overwrite?: boolean }): TPromise<void> {
const existingModel = this.models.get(resource);
return this.fileService.createFile(resource, contents, options).then(() => {
// If we had an existing model for the given resource, load
// it again to make sure it is up to date with the contents
// we just wrote into the underlying resource by calling
// revert()
if (existingModel && !existingModel.isDisposed()) {
return existingModel.revert();
}
return void 0;
});
}
delete(resource: URI, options?: { useTrash?: boolean, recursive?: boolean }): TPromise<void> {
const dirtyFiles = this.getDirty().filter(dirty => isEqualOrParent(dirty, resource, !platform.isLinux /* ignorecase */));
return this.revertAll(dirtyFiles, { soft: true }).then(() => this.fileService.del(resource, options));
}
move(source: URI, target: URI, overwrite?: boolean): TPromise<void> {
const waitForPromises: TPromise[] = [];
this._onWillMove.fire({
oldResource: source,
newResource: target,
waitUntil(p: TPromise<any>) {
waitForPromises.push(TPromise.wrap(p).then(undefined, errors.onUnexpectedError));
}
});
// prevent async waitUntil-calls
Object.freeze(waitForPromises);
return TPromise.join(waitForPromises).then(() => {
// Handle target models if existing (if target URI is a folder, this can be multiple)
let handleTargetModelPromise: TPromise<any> = TPromise.as(void 0);
const dirtyTargetModels = this.getDirtyFileModels().filter(model => isEqualOrParent(model.getResource(), target, !platform.isLinux /* ignorecase */));
if (dirtyTargetModels.length) {
handleTargetModelPromise = this.revertAll(dirtyTargetModels.map(targetModel => targetModel.getResource()), { soft: true });
}
return handleTargetModelPromise.then(() => {
// Handle dirty source models if existing (if source URI is a folder, this can be multiple)
let handleDirtySourceModels: TPromise<any>;
const dirtySourceModels = this.getDirtyFileModels().filter(model => isEqualOrParent(model.getResource(), source, !platform.isLinux /* ignorecase */));
const dirtyTargetModels: URI[] = [];
if (dirtySourceModels.length) {
handleDirtySourceModels = TPromise.join(dirtySourceModels.map(sourceModel => {
const sourceModelResource = sourceModel.getResource();
let targetModelResource: URI;
// If the source is the actual model, just use target as new resource
if (isEqual(sourceModelResource, source, !platform.isLinux /* ignorecase */)) {
targetModelResource = target;
}
// Otherwise a parent folder of the source is being moved, so we need
// to compute the target resource based on that
else {
targetModelResource = sourceModelResource.with({ path: paths.join(target.path, sourceModelResource.path.substr(source.path.length + 1)) });
}
// Remember as dirty target model to load after the operation
dirtyTargetModels.push(targetModelResource);
// Backup dirty source model to the target resource it will become later
return this.backupFileService.backupResource(targetModelResource, sourceModel.createSnapshot(), sourceModel.getVersionId());
}));
} else {
handleDirtySourceModels = TPromise.as(void 0);
}
return handleDirtySourceModels.then(() => {
// Soft revert the dirty source files if any
return this.revertAll(dirtySourceModels.map(dirtySourceModel => dirtySourceModel.getResource()), { soft: true }).then(() => {
// Rename to target
return this.fileService.moveFile(source, target, overwrite).then(() => {
// Load models that were dirty before
return TPromise.join(dirtyTargetModels.map(dirtyTargetModel => this.models.loadOrCreate(dirtyTargetModel))).then(() => void 0);
}, error => {
// In case of an error, discard any dirty target backups that were made
return TPromise.join(dirtyTargetModels.map(dirtyTargetModel => this.backupFileService.discardResourceBackup(dirtyTargetModel)))
.then(() => TPromise.wrapError(error));
});
});
});
});
});
}
getAutoSaveMode(): AutoSaveMode {
if (this.configuredAutoSaveOnFocusChange) {
return AutoSaveMode.ON_FOCUS_CHANGE;
}
@@ -718,7 +808,7 @@ export abstract class TextFileService implements ITextFileService {
return AutoSaveMode.OFF;
}
public getAutoSaveConfiguration(): IAutoSaveConfiguration {
getAutoSaveConfiguration(): IAutoSaveConfiguration {
return {
autoSaveDelay: this.configuredAutoSaveDelay && this.configuredAutoSaveDelay > 0 ? this.configuredAutoSaveDelay : void 0,
autoSaveFocusChange: this.configuredAutoSaveOnFocusChange,
@@ -726,14 +816,15 @@ export abstract class TextFileService implements ITextFileService {
};
}
public get isHotExitEnabled(): boolean {
get isHotExitEnabled(): boolean {
return !this.environmentService.isExtensionDevelopment && this.configuredHotExit !== HotExitConfiguration.OFF;
}
public dispose(): void {
this.toUnbind = dispose(this.toUnbind);
dispose(): void {
// Clear all caches
this._models.clear();
super.dispose();
}
}

View File

@@ -8,12 +8,11 @@ import { TPromise } from 'vs/base/common/winjs.base';
import URI from 'vs/base/common/uri';
import { Event } from 'vs/base/common/event';
import { IDisposable } from 'vs/base/common/lifecycle';
import { IEncodingSupport, ConfirmResult } from 'vs/workbench/common/editor';
import { IEncodingSupport, ConfirmResult, IRevertOptions } from 'vs/workbench/common/editor';
import { IBaseStat, IResolveContentOptions, ITextSnapshot } from 'vs/platform/files/common/files';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { ITextEditorModel } from 'vs/editor/common/services/resolverService';
import { ITextBufferFactory } from 'vs/editor/common/model';
import { IRevertOptions } from 'vs/platform/editor/common/editor';
import { RawContextKey } from 'vs/platform/contextkey/common/contextkey';
/**
@@ -81,11 +80,11 @@ export class TextFileModelChangeEvent {
this._kind = kind;
}
public get resource(): URI {
get resource(): URI {
return this._resource;
}
public get kind(): StateChange {
get kind(): StateChange {
return this._kind;
}
}
@@ -124,6 +123,12 @@ export enum SaveReason {
WINDOW_CHANGE = 4
}
export enum LoadReason {
EDITOR = 1,
REFERENCE = 2,
OTHER = 3
}
export const ITextFileService = createDecorator<ITextFileService>(TEXT_FILE_SERVICE_ID);
export interface IRawTextContent extends IBaseStat {
@@ -141,6 +146,10 @@ export interface IRawTextContent extends IBaseStat {
export interface IModelLoadOrCreateOptions {
/**
* Context why the model is being loaded or created.
*/
reason?: LoadReason;
/**
* The encoding to use when resolving the model text content.
@@ -148,9 +157,16 @@ export interface IModelLoadOrCreateOptions {
encoding?: string;
/**
* Wether to reload the model if it already exists.
* If the model was already loaded before, allows to trigger
* a reload of it to fetch the latest contents:
* - async: loadOrCreate() will return immediately and trigger
* a reload that will run in the background.
* - sync: loadOrCreate() will only return resolved when the
* model has finished reloading.
*/
reload?: boolean;
reload?: {
async: boolean
};
/**
* Allow to load a model even if we think it is a binary file.
@@ -204,6 +220,11 @@ export interface ILoadOptions {
* Allow to load a model even if we think it is a binary file.
*/
allowBinary?: boolean;
/**
* Context why the model is being loaded.
*/
reason?: LoadReason;
}
export interface ITextFileEditorModel extends ITextEditorModel, IEncodingSupport {
@@ -233,18 +254,32 @@ export interface ITextFileEditorModel extends ITextEditorModel, IEncodingSupport
isResolved(): boolean;
isReadonly(): boolean;
isDisposed(): boolean;
}
export interface IWillMoveEvent {
oldResource: URI;
newResource: URI;
waitUntil(p: TPromise<any>): void;
}
export interface ITextFileService extends IDisposable {
_serviceBrand: any;
onAutoSaveConfigurationChange: Event<IAutoSaveConfiguration>;
onFilesAssociationChange: Event<void>;
readonly onAutoSaveConfigurationChange: Event<IAutoSaveConfiguration>;
readonly onFilesAssociationChange: Event<void>;
onWillMove: Event<IWillMoveEvent>;
readonly isHotExitEnabled: boolean;
/**
* Access to the manager of text file editor models providing further methods to work with them.
*/
models: ITextFileEditorModelManager;
readonly models: ITextFileEditorModelManager;
/**
* Resolve the contents of a file identified by the resource.
@@ -308,6 +343,22 @@ export interface ITextFileService extends IDisposable {
*/
revertAll(resources?: URI[], options?: IRevertOptions): TPromise<ITextFileOperationResult>;
/**
* Create a file. If the file exists it will be overwritten with the contents if
* the options enable to overwrite.
*/
create(resource: URI, contents?: string, options?: { overwrite?: boolean }): TPromise<void>;
/**
* Delete a file. If the file is dirty, it will get reverted and then deleted from disk.
*/
delete(resource: URI, options?: { useTrash?: boolean, recursive?: boolean }): TPromise<void>;
/**
* Move a file. If the file is dirty, its contents will be preserved and restored.
*/
move(source: URI, target: URI, overwrite?: boolean): TPromise<void>;
/**
* Brings up the confirm dialog to either save, don't save or cancel.
*
@@ -325,9 +376,4 @@ export interface ITextFileService extends IDisposable {
* Convinient fast access to the raw configured auto save settings.
*/
getAutoSaveConfiguration(): IAutoSaveConfiguration;
/**
* Convinient fast access to the hot exit file setting.
*/
isHotExitEnabled: boolean;
}

View File

@@ -28,9 +28,8 @@ import { IWindowsService, IWindowService } from 'vs/platform/windows/common/wind
import { IHistoryService } from 'vs/workbench/services/history/common/history';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IModelService } from 'vs/editor/common/services/modelService';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
import { getConfirmMessage, IDialogService } from 'vs/platform/dialogs/common/dialogs';
import { Severity } from 'vs/editor/common/standalone/standaloneBase';
export class TextFileService extends AbstractTextFileService {
@@ -55,7 +54,7 @@ export class TextFileService extends AbstractTextFileService {
super(lifecycleService, contextService, configurationService, fileService, untitledEditorService, instantiationService, notificationService, environmentService, backupFileService, windowsService, historyService, contextKeyService, modelService);
}
public resolveTextContent(resource: URI, options?: IResolveContentOptions): TPromise<IRawTextContent> {
resolveTextContent(resource: URI, options?: IResolveContentOptions): TPromise<IRawTextContent> {
return this.fileService.resolveStreamContent(resource, options).then(streamContent => {
return createTextBufferFactoryFromStream(streamContent.value).then(res => {
const r: IRawTextContent = {
@@ -64,6 +63,7 @@ export class TextFileService extends AbstractTextFileService {
mtime: streamContent.mtime,
etag: streamContent.etag,
encoding: streamContent.encoding,
isReadonly: streamContent.isReadonly,
value: res
};
return r;
@@ -71,7 +71,7 @@ export class TextFileService extends AbstractTextFileService {
});
}
public confirmSave(resources?: URI[]): TPromise<ConfirmResult> {
confirmSave(resources?: URI[]): TPromise<ConfirmResult> {
if (this.environmentService.isExtensionDevelopment) {
return TPromise.wrap(ConfirmResult.DONT_SAVE); // no veto when we are in extension dev mode because we cannot assum we run interactive (e.g. tests)
}
@@ -102,7 +102,7 @@ export class TextFileService extends AbstractTextFileService {
});
}
public promptForPath(defaultPath: string): TPromise<string> {
promptForPath(defaultPath: string): TPromise<string> {
return this.windowService.showSaveDialog(this.getSaveDialogOptions(defaultPath));
}

View File

@@ -16,7 +16,11 @@ import { toResource } from 'vs/base/test/common/utils';
import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager';
import { FileOperationResult, FileOperationError, IFileService, snapshotToString } from 'vs/platform/files/common/files';
import { IModelService } from 'vs/editor/common/services/modelService';
import { timeout } from 'vs/base/common/async';
import { timeout as thenableTimeout } from 'vs/base/common/async';
function timeout(n: number) {
return TPromise.wrap(thenableTimeout(n));
}
class ServiceAccessor {
constructor(@ITextFileService public textFileService: TestTextFileService, @IModelService public modelService: IModelService, @IFileService public fileService: TestFileService) {

View File

@@ -10,8 +10,7 @@ import URI from 'vs/base/common/uri';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager';
import { join } from 'vs/base/common/paths';
import { workbenchInstantiationService, TestEditorGroupService, TestFileService } from 'vs/workbench/test/workbenchTestServices';
import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService';
import { workbenchInstantiationService, TestFileService } from 'vs/workbench/test/workbenchTestServices';
import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel';
import { IFileService, FileChangesEvent, FileChangeType } from 'vs/platform/files/common/files';
import { IModelService } from 'vs/editor/common/services/modelService';
@@ -26,7 +25,6 @@ export class TestTextFileEditorModelManager extends TextFileEditorModelManager {
class ServiceAccessor {
constructor(
@IEditorGroupService public editorGroupService: TestEditorGroupService,
@IFileService public fileService: TestFileService,
@IModelService public modelService: IModelService
) {
@@ -107,7 +105,7 @@ suite('Files - TextFileEditorModelManager', () => {
const resource = URI.file('/test.html');
const encoding = 'utf8';
return manager.loadOrCreate(resource, { encoding, reload: true }).then(model => {
return manager.loadOrCreate(resource, { encoding }).then(model => {
assert.ok(model);
assert.equal(model.getEncoding(), encoding);
assert.equal(manager.get(resource), model);

View File

@@ -5,10 +5,12 @@
'use strict';
import * as assert from 'assert';
import * as sinon from 'sinon';
import * as platform from 'vs/base/common/platform';
import URI from 'vs/base/common/uri';
import { TPromise } from 'vs/base/common/winjs.base';
import { ILifecycleService, ShutdownEvent, ShutdownReason } from 'vs/platform/lifecycle/common/lifecycle';
import { workbenchInstantiationService, TestLifecycleService, TestTextFileService, TestWindowsService, TestContextService } from 'vs/workbench/test/workbenchTestServices';
import { workbenchInstantiationService, TestLifecycleService, TestTextFileService, TestWindowsService, TestContextService, TestFileService } from 'vs/workbench/test/workbenchTestServices';
import { toResource } from 'vs/base/test/common/utils';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IWindowsService } from 'vs/platform/windows/common/windows';
@@ -17,9 +19,12 @@ import { ITextFileService } from 'vs/workbench/services/textfile/common/textfile
import { ConfirmResult } from 'vs/workbench/common/editor';
import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService';
import { UntitledEditorModel } from 'vs/workbench/common/editor/untitledEditorModel';
import { HotExitConfiguration } from 'vs/platform/files/common/files';
import { HotExitConfiguration, IFileService } from 'vs/platform/files/common/files';
import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager';
import { IWorkspaceContextService, Workspace } from 'vs/platform/workspace/common/workspace';
import { IModelService } from 'vs/editor/common/services/modelService';
import { ModelServiceImpl } from 'vs/editor/common/services/modelServiceImpl';
import { Schemas } from 'vs/base/common/network';
class ServiceAccessor {
constructor(
@@ -27,7 +32,9 @@ class ServiceAccessor {
@ITextFileService public textFileService: TestTextFileService,
@IUntitledEditorService public untitledEditorService: IUntitledEditorService,
@IWindowsService public windowsService: TestWindowsService,
@IWorkspaceContextService public contextService: TestContextService
@IWorkspaceContextService public contextService: TestContextService,
@IModelService public modelService: ModelServiceImpl,
@IFileService public fileService: TestFileService
) {
}
}
@@ -194,6 +201,34 @@ suite('Files - TextFileService', () => {
});
});
test('save - UNC path', function () {
const untitledUncUri = URI.from({ scheme: 'untitled', authority: 'server', path: '/share/path/file.txt' });
model = instantiationService.createInstance(TextFileEditorModel, untitledUncUri, 'utf8');
(<TextFileEditorModelManager>accessor.textFileService.models).add(model.getResource(), model);
const mockedFileUri = untitledUncUri.with({ scheme: Schemas.file });
const mockedEditorInput = instantiationService.createInstance(TextFileEditorModel, mockedFileUri, 'utf8');
const loadOrCreateStub = sinon.stub(accessor.textFileService.models, 'loadOrCreate', () => TPromise.wrap(mockedEditorInput));
sinon.stub(accessor.untitledEditorService, 'exists', () => true);
sinon.stub(accessor.untitledEditorService, 'hasAssociatedFilePath', () => true);
sinon.stub(accessor.modelService, 'updateModel', () => { });
return model.load().then(() => {
model.textEditorModel.setValue('foo');
return accessor.textFileService.saveAll(true).then(res => {
assert.ok(loadOrCreateStub.calledOnce);
assert.equal(res.results.length, 1);
assert.ok(res.results[0].success);
assert.equal(res.results[0].target.scheme, Schemas.file);
assert.equal(res.results[0].target.authority, untitledUncUri.authority);
assert.equal(res.results[0].target.path, untitledUncUri.path);
});
});
});
test('saveAll - file', function () {
model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8');
(<TextFileEditorModelManager>accessor.textFileService.models).add(model.getResource(), model);
@@ -252,6 +287,45 @@ suite('Files - TextFileService', () => {
});
});
test('delete - dirty file', function () {
model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8');
(<TextFileEditorModelManager>accessor.textFileService.models).add(model.getResource(), model);
const service = accessor.textFileService;
return model.load().then(() => {
model.textEditorModel.setValue('foo');
assert.ok(service.isDirty(model.getResource()));
return service.delete(model.getResource()).then(() => {
assert.ok(!service.isDirty(model.getResource()));
});
});
});
test('move - dirty file', function () {
let sourceModel: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8');
let targetModel: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file_target.txt'), 'utf8');
(<TextFileEditorModelManager>accessor.textFileService.models).add(sourceModel.getResource(), sourceModel);
(<TextFileEditorModelManager>accessor.textFileService.models).add(targetModel.getResource(), targetModel);
const service = accessor.textFileService;
return sourceModel.load().then(() => {
sourceModel.textEditorModel.setValue('foo');
assert.ok(service.isDirty(sourceModel.getResource()));
return service.move(sourceModel.getResource(), targetModel.getResource(), true).then(() => {
assert.ok(!service.isDirty(sourceModel.getResource()));
sourceModel.dispose();
targetModel.dispose();
});
});
});
// {{SQL CARBON EDIT}}
/*
suite('Hot Exit', () => {

View File

@@ -12,7 +12,7 @@ import { ITextModel } from 'vs/editor/common/model';
import { IDisposable, toDisposable, IReference, ReferenceCollection, ImmortalReference } from 'vs/base/common/lifecycle';
import { IModelService } from 'vs/editor/common/services/modelService';
import { ResourceEditorModel } from 'vs/workbench/common/editor/resourceEditorModel';
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
import { ITextFileService, LoadReason } from 'vs/workbench/services/textfile/common/textfiles';
import * as network from 'vs/base/common/network';
import { ITextModelService, ITextModelContentProvider, ITextEditorModel } from 'vs/editor/common/services/resolverService';
import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService';
@@ -31,15 +31,16 @@ class ResourceModelCollection extends ReferenceCollection<TPromise<ITextEditorMo
super();
}
public createReferencedObject(key: string): TPromise<ITextEditorModel> {
createReferencedObject(key: string): TPromise<ITextEditorModel> {
const resource = URI.parse(key);
if (this.fileService.canHandleResource(resource)) {
return this.textFileService.models.loadOrCreate(resource);
return this.textFileService.models.loadOrCreate(resource, { reason: LoadReason.REFERENCE });
}
return this.resolveTextModelContent(key).then(() => this.instantiationService.createInstance(ResourceEditorModel, resource));
}
public destroyReferencedObject(modelPromise: TPromise<ITextEditorModel>): void {
destroyReferencedObject(modelPromise: TPromise<ITextEditorModel>): void {
modelPromise.done(model => {
if (model instanceof TextFileEditorModel) {
this.textFileService.models.disposeModel(model);
@@ -51,7 +52,7 @@ class ResourceModelCollection extends ReferenceCollection<TPromise<ITextEditorMo
});
}
public registerTextModelContentProvider(scheme: string, provider: ITextModelContentProvider): IDisposable {
registerTextModelContentProvider(scheme: string, provider: ITextModelContentProvider): IDisposable {
const registry = this.providers;
const providers = registry[scheme] || (registry[scheme] = []);
@@ -108,21 +109,19 @@ export class TextModelResolverService implements ITextModelService {
this.resourceModelCollection = instantiationService.createInstance(ResourceModelCollection);
}
public createModelReference(resource: URI): TPromise<IReference<ITextEditorModel>> {
createModelReference(resource: URI): TPromise<IReference<ITextEditorModel>> {
return this._createModelReference(resource);
}
private _createModelReference(resource: URI): TPromise<IReference<ITextEditorModel>> {
// Untitled Schema: go through cached input
// TODO ImmortalReference is a hack
if (resource.scheme === network.Schemas.untitled) {
return this.untitledEditorService.loadOrCreate({ resource }).then(model => new ImmortalReference(model));
}
// InMemory Schema: go through model service cache
// TODO ImmortalReference is a hack
if (resource.scheme === 'inmemory') {
if (resource.scheme === network.Schemas.inMemory) {
const cachedModel = this.modelService.getModel(resource);
if (!cachedModel) {
@@ -144,7 +143,7 @@ export class TextModelResolverService implements ITextModelService {
);
}
public registerTextModelContentProvider(scheme: string, provider: ITextModelContentProvider): IDisposable {
registerTextModelContentProvider(scheme: string, provider: ITextModelContentProvider): IDisposable {
return this.resourceModelCollection.registerTextModelContentProvider(scheme, provider);
}
}

View File

@@ -133,12 +133,12 @@ suite('Workbench - TextModelResolverService', () => {
let waitForIt = new TPromise(c => resolveModel = c);
const disposable = accessor.textModelResolverService.registerTextModelContentProvider('test', {
provideTextContent: async (resource: URI): TPromise<ITextModel> => {
await waitForIt;
let modelContent = 'Hello Test';
let mode = accessor.modeService.getOrCreateMode('json');
return accessor.modelService.createModel(modelContent, mode, resource);
provideTextContent: (resource: URI): TPromise<ITextModel> => {
return waitForIt.then(_ => {
let modelContent = 'Hello Test';
let mode = accessor.modeService.getOrCreateMode('json');
return accessor.modelService.createModel(modelContent, mode, resource);
});
}
});

View File

@@ -186,7 +186,7 @@ export function tokenColorsSchema(description: string): IJSONSchema {
};
}
const schemaId = 'vscode://schemas/color-theme';
export const schemaId = 'vscode://schemas/color-theme';
const schema: IJSONSchema = {
type: 'object',
allowComments: true,

Some files were not shown because too many files have changed in this diff Show More