mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-10 02:02:35 -05:00
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:
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
617
src/vs/workbench/services/editor/browser/editorService.ts
Normal file
617
src/vs/workbench/services/editor/browser/editorService.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
@@ -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 [];
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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> {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 }]);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
479
src/vs/workbench/services/group/common/editorGroupsService.ts
Normal file
479
src/vs/workbench/services/group/common/editorGroupsService.ts
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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[]>();
|
||||
}
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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 || []));
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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) };
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
39
src/vs/workbench/services/progress/common/progress.ts
Normal file
39
src/vs/workbench/services/progress/common/progress.ts
Normal 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;
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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({
|
||||
|
||||
@@ -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')]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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() + '])');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user