diff --git a/build/win32/code.iss b/build/win32/code.iss index 1601f540fa2..4230f7d0517 100644 --- a/build/win32/code.iss +++ b/build/win32/code.iss @@ -971,13 +971,18 @@ begin Result := not IsBackgroundUpdate(); end; +// VS Code will create a flag file before the update starts (/update=C:\foo\bar) +// - if the file exists at this point, the user quit Code before the update finished, so don't start Code after update +// - otherwise, the user has accepted to apply the update and Code should start +function LockFileExists(): Boolean; +begin + Result := FileExists(ExpandConstant('{param:update}')) +end; + function ShouldRunAfterUpdate(): Boolean; begin if IsBackgroundUpdate() then - // VS Code will create a flag file before the update starts (/update=C:\foo\bar) - // - if the file exists at this point, the user quit Code before the update finished, so don't start Code after update - // - otherwise, the user has accepted to apply the update and Code should start - Result := not FileExists(ExpandConstant('{param:update}')) + Result := not LockFileExists() else Result := True; end; @@ -998,6 +1003,14 @@ begin Result := ExpandConstant('{app}'); end; +function BoolToStr(Value: Boolean): String; +begin + if Value then + Result := 'true' + else + Result := 'false'; +end; + procedure CurStepChanged(CurStep: TSetupStep); var UpdateResultCode: Integer; @@ -1012,7 +1025,7 @@ begin Sleep(1000); end; - Exec(ExpandConstant('{app}\inno_updater.exe'), ExpandConstant('--apply-update _ "{app}\unins000.dat"'), '', SW_SHOW, ewWaitUntilTerminated, UpdateResultCode); + Exec(ExpandConstant('{app}\inno_updater.exe'), ExpandConstant('_ "{app}\unins000.dat" ' + BoolToStr(LockFileExists())), '', SW_SHOW, ewWaitUntilTerminated, UpdateResultCode); end; end; diff --git a/build/win32/inno_updater.exe b/build/win32/inno_updater.exe index d82b1430c64..6b8c7a98142 100644 Binary files a/build/win32/inno_updater.exe and b/build/win32/inno_updater.exe differ diff --git a/package.json b/package.json index dc5b1d8acfc..744c612172f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.20.0", - "distro": "2478cca5311e147817eef29cb81c0995a3517842", + "distro": "e14a5a3afaae557ff651b344462ed39e776435a2", "author": { "name": "Microsoft Corporation" }, @@ -129,4 +129,4 @@ "windows-mutex": "^0.2.0", "windows-process-tree": "0.1.6" } -} +} \ No newline at end of file diff --git a/src/vs/code/electron-browser/sharedProcess/contrib/languagePackExtensions.ts b/src/vs/code/electron-browser/sharedProcess/contrib/languagePackExtensions.ts index 64fd8395763..824a6c2ee62 100644 --- a/src/vs/code/electron-browser/sharedProcess/contrib/languagePackExtensions.ts +++ b/src/vs/code/electron-browser/sharedProcess/contrib/languagePackExtensions.ts @@ -71,10 +71,10 @@ export class LanguagePackExtensions extends Disposable { if (extension && extension.manifest && extension.manifest.contributes && extension.manifest.contributes.localizations && extension.manifest.contributes.localizations.length) { const extensionIdentifier = { id: getGalleryExtensionIdFromLocal(extension), uuid: extension.identifier.uuid }; for (const localizationContribution of extension.manifest.contributes.localizations) { - if (localizationContribution.languagId && localizationContribution.translations) { - const languageSources = languagePacks[localizationContribution.languagId] || []; + if (localizationContribution.languageId && localizationContribution.translations) { + const languageSources = languagePacks[localizationContribution.languageId] || []; languageSources.splice(0, 0, { extensionIdentifier, translations: join(extension.path, localizationContribution.translations), version: extension.manifest.version }); - languagePacks[localizationContribution.languagId] = languageSources; + languagePacks[localizationContribution.languageId] = languageSources; } } } diff --git a/src/vs/code/electron-main/window.ts b/src/vs/code/electron-main/window.ts index 4c37dd6e993..e79a7400e20 100644 --- a/src/vs/code/electron-main/window.ts +++ b/src/vs/code/electron-main/window.ts @@ -953,7 +953,7 @@ export class CodeWindow implements ICodeWindow { const segments: ITouchBarSegment[] = items.map(item => { let icon: Electron.NativeImage; if (item.iconPath) { - icon = nativeImage.createFromPath(item.iconPath); + icon = nativeImage.createFromPath(item.iconPath.dark); if (icon.isEmpty()) { icon = void 0; } diff --git a/src/vs/code/electron-main/windows.ts b/src/vs/code/electron-main/windows.ts index 8ecf8499910..b1bdea73fb4 100644 --- a/src/vs/code/electron-main/windows.ts +++ b/src/vs/code/electron-main/windows.ts @@ -1318,7 +1318,10 @@ export class WindowsManager implements IWindowsMainService { } if (e.window.config && !!e.window.config.extensionDevelopmentPath) { - return; // do not ask to save workspace when doing extension development + // do not ask to save workspace when doing extension development + // but still delete it. + this.workspacesMainService.deleteUntitledWorkspaceSync(workspace); + return; } if (windowClosing && !isMacintosh && this.getWindowCount() === 1) { @@ -1716,8 +1719,8 @@ class Dialogs { class WorkspacesManager { constructor( - private workspacesService: IWorkspacesMainService, - private backupService: IBackupMainService, + private workspacesMainService: IWorkspacesMainService, + private backupMainService: IBackupMainService, private environmentService: IEnvironmentService, private windowsMainService: IWindowsMainService ) { @@ -1741,7 +1744,7 @@ class WorkspacesManager { return TPromise.as(null); // return early if the workspace is not valid } - return this.workspacesService.createWorkspace(folders).then(workspace => { + return this.workspacesMainService.createWorkspace(folders).then(workspace => { return this.doSaveAndOpenWorkspace(window, workspace, path); }); }); @@ -1758,7 +1761,7 @@ class WorkspacesManager { } // Prevent overwriting a workspace that is currently opened in another window - if (findWindowOnWorkspace(this.windowsMainService.getWindows(), { id: this.workspacesService.getWorkspaceId(path), configPath: path })) { + if (findWindowOnWorkspace(this.windowsMainService.getWindows(), { id: this.workspacesMainService.getWorkspaceId(path), configPath: path })) { const options: Electron.MessageBoxOptions = { title: product.nameLong, type: 'info', @@ -1777,7 +1780,7 @@ class WorkspacesManager { private doSaveAndOpenWorkspace(window: CodeWindow, workspace: IWorkspaceIdentifier, path?: string): TPromise { let savePromise: TPromise; if (path) { - savePromise = this.workspacesService.saveWorkspace(workspace, path); + savePromise = this.workspacesMainService.saveWorkspace(workspace, path); } else { savePromise = TPromise.as(workspace); } @@ -1788,7 +1791,7 @@ class WorkspacesManager { // Register window for backups and migrate current backups over let backupPath: string; if (!window.config.extensionDevelopmentPath) { - backupPath = this.backupService.registerWorkspaceBackupSync(workspace, window.config.backupPath); + backupPath = this.backupMainService.registerWorkspaceBackupSync(workspace, window.config.backupPath); } // Update window configuration properly based on transition to workspace @@ -1861,7 +1864,7 @@ class WorkspacesManager { // Don't Save: delete workspace case ConfirmResult.DONT_SAVE: - this.workspacesService.deleteUntitledWorkspaceSync(workspace); + this.workspacesMainService.deleteUntitledWorkspaceSync(workspace); return false; // Save: save workspace, but do not veto unload @@ -1873,7 +1876,7 @@ class WorkspacesManager { defaultPath: this.getUntitledWorkspaceSaveDialogDefaultPath(workspace) }, window).then(target => { if (target) { - return this.workspacesService.saveWorkspace(workspace, target).then(() => false, () => false); + return this.workspacesMainService.saveWorkspace(workspace, target).then(() => false, () => false); } return true; // keep veto if no target was provided @@ -1889,7 +1892,7 @@ class WorkspacesManager { return dirname(workspace); } - const resolvedWorkspace = this.workspacesService.resolveWorkspaceSync(workspace.configPath); + const resolvedWorkspace = this.workspacesMainService.resolveWorkspaceSync(workspace.configPath); if (resolvedWorkspace && resolvedWorkspace.folders.length > 0) { for (const folder of resolvedWorkspace.folders) { if (folder.uri.scheme === Schemas.file) { diff --git a/src/vs/editor/browser/viewParts/viewCursors/viewCursor.ts b/src/vs/editor/browser/viewParts/viewCursors/viewCursor.ts index 92838c8a6d7..8c93fd9ef2b 100644 --- a/src/vs/editor/browser/viewParts/viewCursors/viewCursor.ts +++ b/src/vs/editor/browser/viewParts/viewCursors/viewCursor.ts @@ -34,7 +34,6 @@ class ViewCursorRenderData { export class ViewCursor { private readonly _context: ViewContext; - private readonly _isSecondary: boolean; private readonly _domNode: FastDomNode; private _cursorStyle: TextEditorCursorStyle; @@ -49,9 +48,8 @@ export class ViewCursor { private _lastRenderedContent: string; private _renderData: ViewCursorRenderData; - constructor(context: ViewContext, isSecondary: boolean) { + constructor(context: ViewContext) { this._context = context; - this._isSecondary = isSecondary; this._cursorStyle = this._context.configuration.editor.viewInfo.cursorStyle; this._lineHeight = this._context.configuration.editor.lineHeight; @@ -62,11 +60,7 @@ export class ViewCursor { // Create the dom node this._domNode = createFastDomNode(document.createElement('div')); - if (this._isSecondary) { - this._domNode.setClassName('cursor secondary'); - } else { - this._domNode.setClassName('cursor'); - } + this._domNode.setClassName('cursor'); this._domNode.setHeight(this._lineHeight); this._domNode.setTop(0); this._domNode.setLeft(0); diff --git a/src/vs/editor/browser/viewParts/viewCursors/viewCursors.css b/src/vs/editor/browser/viewParts/viewCursors/viewCursors.css index 4fc3fbb24c6..a0ecc219714 100644 --- a/src/vs/editor/browser/viewParts/viewCursors/viewCursors.css +++ b/src/vs/editor/browser/viewParts/viewCursors/viewCursors.css @@ -12,9 +12,6 @@ cursor: text; overflow: hidden; } -.monaco-editor .cursors-layer > .cursor.secondary { - opacity: 0.6; -} /* -- block-outline-style -- */ .monaco-editor .cursors-layer.cursor-block-outline-style > .cursor { diff --git a/src/vs/editor/browser/viewParts/viewCursors/viewCursors.ts b/src/vs/editor/browser/viewParts/viewCursors/viewCursors.ts index c54b332a631..98ec0b5a063 100644 --- a/src/vs/editor/browser/viewParts/viewCursors/viewCursors.ts +++ b/src/vs/editor/browser/viewParts/viewCursors/viewCursors.ts @@ -49,7 +49,7 @@ export class ViewCursors extends ViewPart { this._cursorStyle = this._context.configuration.editor.viewInfo.cursorStyle; this._selectionIsEmpty = true; - this._primaryCursor = new ViewCursor(this._context, false); + this._primaryCursor = new ViewCursor(this._context); this._secondaryCursors = []; this._renderData = []; @@ -109,7 +109,7 @@ export class ViewCursors extends ViewPart { // Create new cursors let addCnt = secondaryPositions.length - this._secondaryCursors.length; for (let i = 0; i < addCnt; i++) { - let newCursor = new ViewCursor(this._context, true); + let newCursor = new ViewCursor(this._context); this._domNode.domNode.insertBefore(newCursor.getDomNode().domNode, this._primaryCursor.getDomNode().domNode.nextSibling); this._secondaryCursors.push(newCursor); } diff --git a/src/vs/editor/common/config/fontInfo.ts b/src/vs/editor/common/config/fontInfo.ts index af6a2d82873..24b105bbd4e 100644 --- a/src/vs/editor/common/config/fontInfo.ts +++ b/src/vs/editor/common/config/fontInfo.ts @@ -14,6 +14,16 @@ import { EDITOR_FONT_DEFAULTS } from 'vs/editor/common/config/editorOptions'; */ const GOLDEN_LINE_HEIGHT_RATIO = platform.isMacintosh ? 1.5 : 1.35; +/** + * Font settings maximum and minimum limits + */ +const MINIMUM_FONT_SIZE = 8; +const MAXIMUM_FONT_SIZE = 100; +const MINIMUM_LINE_HEIGHT = 8; +const MAXIMUM_LINE_HEIGHT = 150; +const MINIMUM_LETTER_SPACING = -5; +const MAXIMUM_LETTER_SPACING = 20; + function safeParseFloat(n: number | string, defaultValue: number): number { if (typeof n === 'number') { return n; @@ -70,24 +80,25 @@ export class BareFontInfo { let fontFamily = _string(opts.fontFamily, EDITOR_FONT_DEFAULTS.fontFamily); let fontWeight = _string(opts.fontWeight, EDITOR_FONT_DEFAULTS.fontWeight); + let fontSize = safeParseFloat(opts.fontSize, EDITOR_FONT_DEFAULTS.fontSize); - fontSize = clamp(fontSize, 0, 100); + fontSize = clamp(fontSize, 0, MAXIMUM_FONT_SIZE); if (fontSize === 0) { fontSize = EDITOR_FONT_DEFAULTS.fontSize; - } else if (fontSize < 8) { - fontSize = 8; + } else if (fontSize < MINIMUM_FONT_SIZE) { + fontSize = MINIMUM_FONT_SIZE; } let lineHeight = safeParseInt(opts.lineHeight, 0); - lineHeight = clamp(lineHeight, 0, 150); + lineHeight = clamp(lineHeight, 0, MAXIMUM_LINE_HEIGHT); if (lineHeight === 0) { lineHeight = Math.round(GOLDEN_LINE_HEIGHT_RATIO * fontSize); - } else if (lineHeight < 8) { - lineHeight = 8; + } else if (lineHeight < MINIMUM_LINE_HEIGHT) { + lineHeight = MINIMUM_LINE_HEIGHT; } let letterSpacing = safeParseFloat(opts.letterSpacing, 0); - letterSpacing = clamp(letterSpacing, -20, 20); + letterSpacing = clamp(letterSpacing, MINIMUM_LETTER_SPACING, MAXIMUM_LETTER_SPACING); let editorZoomLevelMultiplier = 1 + (EditorZoom.getZoomLevel() * 0.1); fontSize *= editorZoomLevelMultiplier; diff --git a/src/vs/editor/contrib/bracketMatching/bracketMatching.ts b/src/vs/editor/contrib/bracketMatching/bracketMatching.ts index 45a12373ca1..98de48b6899 100644 --- a/src/vs/editor/contrib/bracketMatching/bracketMatching.ts +++ b/src/vs/editor/contrib/bracketMatching/bracketMatching.ts @@ -14,7 +14,7 @@ import { Position } from 'vs/editor/common/core/position'; import { Selection } from 'vs/editor/common/core/selection'; import { RunOnceScheduler } from 'vs/base/common/async'; import * as editorCommon from 'vs/editor/common/editorCommon'; -import { registerEditorAction, registerEditorContribution, ServicesAccessor, EditorAction } from 'vs/editor/browser/editorExtensions'; +import { EditorAction, registerEditorAction, registerEditorContribution, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { editorBracketMatchBackground, editorBracketMatchBorder } from 'vs/editor/common/view/editorColorRegistry'; @@ -22,7 +22,7 @@ import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { TrackedRangeStickiness, IModelDeltaDecoration } from 'vs/editor/common/model'; -class SelectBracketAction extends EditorAction { +class JumpToBracketAction extends EditorAction { constructor() { super({ id: 'editor.action.jumpToBracket', @@ -45,6 +45,25 @@ class SelectBracketAction extends EditorAction { } } +class SelectToBracketAction extends EditorAction { + constructor() { + super({ + id: 'editor.action.selectToBracket', + label: nls.localize('smartSelect.selectToBracket', "Select to Bracket"), + alias: 'Select to Bracket', + precondition: null + }); + } + + public run(accessor: ServicesAccessor, editor: ICodeEditor): void { + let controller = BracketMatchingController.get(editor); + if (!controller) { + return; + } + controller.selectToBracket(); + } +} + type Brackets = [Range, Range]; class BracketsData { @@ -149,6 +168,50 @@ export class BracketMatchingController extends Disposable implements editorCommo this._editor.revealRange(newSelections[0]); } + public selectToBracket(): void { + const model = this._editor.getModel(); + if (!model) { + return; + } + const selection = this._editor.getSelection(); + if (!selection.isEmpty()) { + return; + } + + const position = selection.getStartPosition(); + + let brackets = model.matchBracket(position); + + let openBracket: Position = null; + let closeBracket: Position = null; + + if (!brackets) { + const nextBracket = model.findNextBracket(position); + if (nextBracket && nextBracket.range) { + brackets = model.matchBracket(nextBracket.range.getStartPosition()); + } + } + + if (brackets) { + if (brackets[0].startLineNumber === brackets[1].startLineNumber) { + openBracket = brackets[1].startColumn < brackets[0].startColumn ? + brackets[1].getStartPosition() : brackets[0].getStartPosition(); + closeBracket = brackets[1].startColumn < brackets[0].startColumn ? + brackets[0].getEndPosition() : brackets[1].getEndPosition(); + } else { + openBracket = brackets[1].startLineNumber < brackets[0].startLineNumber ? + brackets[1].getStartPosition() : brackets[0].getStartPosition(); + closeBracket = brackets[1].startLineNumber < brackets[0].startLineNumber ? + brackets[0].getEndPosition() : brackets[1].getEndPosition(); + } + } + + if (openBracket && closeBracket) { + this._editor.setSelection(new Range(openBracket.lineNumber, openBracket.column, closeBracket.lineNumber, closeBracket.column)); + } + } + + private static readonly _DECORATION_OPTIONS = ModelDecorationOptions.register({ stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, className: 'bracket-match' @@ -228,7 +291,8 @@ export class BracketMatchingController extends Disposable implements editorCommo } registerEditorContribution(BracketMatchingController); -registerEditorAction(SelectBracketAction); +registerEditorAction(SelectToBracketAction); +registerEditorAction(JumpToBracketAction); registerThemingParticipant((theme, collector) => { let bracketMatchBackground = theme.getColor(editorBracketMatchBackground); if (bracketMatchBackground) { diff --git a/src/vs/editor/contrib/bracketMatching/test/bracketMatching.test.ts b/src/vs/editor/contrib/bracketMatching/test/bracketMatching.test.ts index 5a90a324fae..cecb3d534d2 100644 --- a/src/vs/editor/contrib/bracketMatching/test/bracketMatching.test.ts +++ b/src/vs/editor/contrib/bracketMatching/test/bracketMatching.test.ts @@ -7,6 +7,7 @@ import * as assert from 'assert'; import { withTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; import { Position } from 'vs/editor/common/core/position'; +import { Selection } from 'vs/editor/common/core/selection'; import { TextModel } from 'vs/editor/common/model/textModel'; import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; import { MockMode } from 'vs/editor/test/common/mocks/mockMode'; @@ -98,4 +99,49 @@ suite('bracket matching', () => { model.dispose(); mode.dispose(); }); + + test('Select to next bracket', () => { + let mode = new BracketMode(); + let model = TextModel.createFromString('var x = (3 + (5-7)); y();', undefined, mode.getLanguageIdentifier()); + + withTestCodeEditor(null, { model: model }, (editor, cursor) => { + let bracketMatchingController = editor.registerAndInstantiateContribution(BracketMatchingController); + + + // start position in open brackets + editor.setPosition(new Position(1, 9)); + bracketMatchingController.selectToBracket(); + assert.deepEqual(editor.getPosition(), new Position(1, 20)); + assert.deepEqual(editor.getSelection(), new Selection(1, 9, 1, 20)); + + // start position in close brackets + editor.setPosition(new Position(1, 20)); + bracketMatchingController.selectToBracket(); + assert.deepEqual(editor.getPosition(), new Position(1, 20)); + assert.deepEqual(editor.getSelection(), new Selection(1, 9, 1, 20)); + + // start position between brackets + editor.setPosition(new Position(1, 16)); + bracketMatchingController.selectToBracket(); + assert.deepEqual(editor.getPosition(), new Position(1, 19)); + assert.deepEqual(editor.getSelection(), new Selection(1, 14, 1, 19)); + + // start position outside brackets + editor.setPosition(new Position(1, 21)); + bracketMatchingController.selectToBracket(); + assert.deepEqual(editor.getPosition(), new Position(1, 25)); + assert.deepEqual(editor.getSelection(), new Selection(1, 23, 1, 25)); + + // do not break if no brackets are available + editor.setPosition(new Position(1, 26)); + bracketMatchingController.selectToBracket(); + assert.deepEqual(editor.getPosition(), new Position(1, 26)); + assert.deepEqual(editor.getSelection(), new Selection(1, 26, 1, 26)); + + bracketMatchingController.dispose(); + }); + + model.dispose(); + mode.dispose(); + }); }); diff --git a/src/vs/editor/standalone/browser/standaloneServices.ts b/src/vs/editor/standalone/browser/standaloneServices.ts index 8071e7863ea..552156aca88 100644 --- a/src/vs/editor/standalone/browser/standaloneServices.ts +++ b/src/vs/editor/standalone/browser/standaloneServices.ts @@ -40,6 +40,7 @@ import { ContextKeyService } from 'vs/platform/contextkey/browser/contextKeyServ import { IMenuService } from 'vs/platform/actions/common/actions'; import { IStandaloneThemeService } from 'vs/editor/standalone/common/standaloneThemeService'; import { StandaloneThemeServiceImpl } from 'vs/editor/standalone/browser/standaloneThemeServiceImpl'; +import { ILogService, NullLogService } from 'vs/platform/log/common/log'; export interface IEditorContextViewService extends IContextViewService { dispose(): void; @@ -142,6 +143,8 @@ export module StaticServices { export const storageService = define(IStorageService, () => NullStorageService); + export const logService = define(ILogService, () => new NullLogService()); + } export class DynamicStandaloneServices extends Disposable { diff --git a/src/vs/platform/actions/browser/menuItemActionItem.ts b/src/vs/platform/actions/browser/menuItemActionItem.ts index 5d81043100e..2ce14fddef7 100644 --- a/src/vs/platform/actions/browser/menuItemActionItem.ts +++ b/src/vs/platform/actions/browser/menuItemActionItem.ts @@ -7,7 +7,7 @@ import { localize } from 'vs/nls'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { IMenu, MenuItemAction, IMenuActionOptions } from 'vs/platform/actions/common/actions'; +import { IMenu, MenuItemAction, IMenuActionOptions, ICommandAction } from 'vs/platform/actions/common/actions'; import { IMessageService } from 'vs/platform/message/common/message'; import Severity from 'vs/base/common/severity'; import { IAction } from 'vs/base/common/actions'; @@ -17,6 +17,9 @@ import { domEvent } from 'vs/base/browser/event'; import { Emitter } from 'vs/base/common/event'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { memoize } from 'vs/base/common/decorators'; +import { IdGenerator } from 'vs/base/common/idGenerator'; +import { createCSSRule } from 'vs/base/browser/dom'; +import URI from 'vs/base/common/uri'; class AltKeyEmitter extends Emitter { @@ -114,17 +117,22 @@ export function createActionItem(action: IAction, keybindingService: IKeybinding return undefined; } +const ids = new IdGenerator('menu-item-action-item-icon-'); + export class MenuItemActionItem extends ActionItem { + static readonly ICON_PATH_TO_CSS_RULES: Map = new Map(); + private _wantsAltCommand: boolean = false; + private _itemClassDispose: IDisposable; constructor( - action: MenuItemAction, + public _action: MenuItemAction, @IKeybindingService private _keybindingService: IKeybindingService, @IMessageService protected _messageService: IMessageService, @IContextMenuService private _contextMenuService: IContextMenuService ) { - super(undefined, action, { icon: !!action.class, label: !action.class }); + super(undefined, _action, { icon: !!(_action.class || _action.item.iconPath), label: !_action.class && !_action.item.iconPath }); } protected get _commandAction(): IAction { @@ -142,6 +150,8 @@ export class MenuItemActionItem extends ActionItem { render(container: HTMLElement): void { super.render(container); + this._updateItemClass(this._action.item); + let mouseOver = false; let altDown = false; @@ -189,13 +199,41 @@ export class MenuItemActionItem extends ActionItem { _updateClass(): void { if (this.options.icon) { - const element = this.$e.getHTMLElement(); if (this._commandAction !== this._action) { - element.classList.remove(this._action.class); + this._updateItemClass(this._action.alt.item); } else if ((this._action).alt) { - element.classList.remove((this._action).alt.class); + this._updateItemClass(this._action.item); } - element.classList.add('icon', this._commandAction.class); } } + + _updateItemClass(item: ICommandAction): void { + dispose(this._itemClassDispose); + this._itemClassDispose = undefined; + + if (item.iconPath) { + let iconClass: string; + + if (MenuItemActionItem.ICON_PATH_TO_CSS_RULES.has(item.iconPath.dark)) { + iconClass = MenuItemActionItem.ICON_PATH_TO_CSS_RULES.get(item.iconPath.dark); + } else { + iconClass = ids.nextId(); + createCSSRule(`.icon.${iconClass}`, `background-image: url("${URI.file(item.iconPath.light || item.iconPath.dark).toString()}")`); + createCSSRule(`.vs-dark .icon.${iconClass}, .hc-black .icon.${iconClass}`, `background-image: url("${URI.file(item.iconPath.dark).toString()}")`); + MenuItemActionItem.ICON_PATH_TO_CSS_RULES.set(item.iconPath.dark, iconClass); + } + + this.$e.getHTMLElement().classList.add('icon', iconClass); + this._itemClassDispose = { dispose: () => this.$e.getHTMLElement().classList.remove('icon', iconClass) }; + } + } + + dispose(): void { + if (this._itemClassDispose) { + dispose(this._itemClassDispose); + this._itemClassDispose = undefined; + } + + super.dispose(); + } } diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index 5e0eaceed36..f8786606a24 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -23,8 +23,7 @@ export interface ICommandAction { id: string; title: string | ILocalizedString; category?: string | ILocalizedString; - iconClass?: string; - iconPath?: string; + iconPath?: { dark: string; light?: string; }; precondition?: ContextKeyExpr; } @@ -180,7 +179,7 @@ export class MenuItemAction extends ExecuteCommandAction { @ICommandService commandService: ICommandService ) { typeof item.title === 'string' ? super(item.id, item.title, commandService) : super(item.id, item.title.value, commandService); - this._cssClass = item.iconClass; + this._cssClass = undefined; this._enabled = !item.precondition || contextKeyService.contextMatchesRules(item.precondition); this._options = options || {}; diff --git a/src/vs/platform/actions/electron-browser/menusExtensionPoint.ts b/src/vs/platform/actions/electron-browser/menusExtensionPoint.ts index 2295ddd7b90..3955afd293e 100644 --- a/src/vs/platform/actions/electron-browser/menusExtensionPoint.ts +++ b/src/vs/platform/actions/electron-browser/menusExtensionPoint.ts @@ -4,12 +4,9 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import URI from 'vs/base/common/uri'; -import { createCSSRule } from 'vs/base/browser/dom'; import { localize } from 'vs/nls'; import { isFalsyOrWhitespace } from 'vs/base/common/strings'; import { join } from 'path'; -import { IdGenerator } from 'vs/base/common/idGenerator'; import { IJSONSchema } from 'vs/base/common/jsonSchema'; import { forEach } from 'vs/base/common/collections'; import { IExtensionPointUser, ExtensionMessageCollector, ExtensionsRegistry } from 'vs/platform/extensions/common/extensionsRegistry'; @@ -281,33 +278,27 @@ namespace schema { ExtensionsRegistry.registerExtensionPoint('commands', [], schema.commandsContribution).setHandler(extensions => { - const ids = new IdGenerator('contrib-cmd-icon-'); - function handleCommand(userFriendlyCommand: schema.IUserFriendlyCommand, extension: IExtensionPointUser) { if (!schema.isValidCommand(userFriendlyCommand, extension.collector)) { return; } - let { icon, category, title, command } = userFriendlyCommand; - let iconClass: string; - let iconPath: string; - if (icon) { - iconClass = ids.nextId(); - if (typeof icon === 'string') { - iconPath = join(extension.description.extensionFolderPath, icon); - createCSSRule(`.icon.${iconClass}`, `background-image: url("${URI.file(iconPath).toString()}")`); - } else { - const light = join(extension.description.extensionFolderPath, icon.light); - const dark = join(extension.description.extensionFolderPath, icon.dark); - createCSSRule(`.icon.${iconClass}`, `background-image: url("${URI.file(light).toString()}")`); - createCSSRule(`.vs-dark .icon.${iconClass}, .hc-black .icon.${iconClass}`, `background-image: url("${URI.file(dark).toString()}")`); + const { icon, category, title, command } = userFriendlyCommand; - iconPath = join(extension.description.extensionFolderPath, icon.dark); + let absoluteIcon: { dark: string; light?: string; }; + if (icon) { + if (typeof icon === 'string') { + absoluteIcon = { dark: join(extension.description.extensionFolderPath, icon) }; + } else { + absoluteIcon = { + dark: join(extension.description.extensionFolderPath, icon.dark), + light: join(extension.description.extensionFolderPath, icon.light) + }; } } - if (MenuRegistry.addCommand({ id: command, title, category, iconClass, iconPath })) { + if (MenuRegistry.addCommand({ id: command, title, category, iconPath: absoluteIcon })) { extension.collector.info(localize('dup', "Command `{0}` appears multiple times in the `commands` section.", userFriendlyCommand.command)); } } diff --git a/src/vs/platform/contextkey/common/contextkey.ts b/src/vs/platform/contextkey/common/contextkey.ts index 5461a40cf07..6a3155f7aad 100644 --- a/src/vs/platform/contextkey/common/contextkey.ts +++ b/src/vs/platform/contextkey/common/contextkey.ts @@ -6,7 +6,6 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import Event from 'vs/base/common/event'; -import { match } from 'vs/base/common/glob'; export enum ContextKeyExprType { Defined = 1, @@ -14,7 +13,7 @@ export enum ContextKeyExprType { Equals = 3, NotEquals = 4, And = 5, - Glob = 6 + Regex = 6 } export abstract class ContextKeyExpr { @@ -31,8 +30,8 @@ export abstract class ContextKeyExpr { return new ContextKeyNotEqualsExpr(key, value); } - public static glob(key: string, value: string): ContextKeyExpr { - return new ContextKeyGlobExpr(key, value); + public static regex(key: string, value: string): ContextKeyExpr { + return new ContextKeyRegexExpr(key, value); } public static not(key: string): ContextKeyExpr { @@ -68,7 +67,7 @@ export abstract class ContextKeyExpr { if (serializedOne.indexOf('=~') >= 0) { let pieces = serializedOne.split('=~'); - return new ContextKeyGlobExpr(pieces[0].trim(), this._deserializeValue(pieces[1])); + return new ContextKeyRegexExpr(pieces[0].trim(), this._deserializeValue(pieces[1])); } if (/^\!\s*/.test(serializedOne)) { @@ -120,8 +119,8 @@ function cmp(a: ContextKeyExpr, b: ContextKeyExpr): number { return (a).cmp(b); case ContextKeyExprType.NotEquals: return (a).cmp(b); - case ContextKeyExprType.Glob: - return (a).cmp(b); + case ContextKeyExprType.Regex: + return (a).cmp(b); default: throw new Error('Unknown ContextKeyExpr!'); } @@ -333,40 +332,48 @@ export class ContextKeyNotExpr implements ContextKeyExpr { } } -export class ContextKeyGlobExpr implements ContextKeyExpr { +export class ContextKeyRegexExpr implements ContextKeyExpr { - constructor(private key: string, private value: any) { + private regexp: { source: string, test(s: string): boolean }; + + constructor(private key: string, value: any) { + try { + this.regexp = new RegExp(value); + } catch (e) { + this.regexp = { source: '', test() { return false; } }; + console.warn(`Bad value for glob-context key expression: ${value}`); + } } public getType(): ContextKeyExprType { - return ContextKeyExprType.Glob; + return ContextKeyExprType.Regex; } - public cmp(other: ContextKeyGlobExpr): number { + public cmp(other: ContextKeyRegexExpr): number { if (this.key < other.key) { return -1; } if (this.key > other.key) { return 1; } - if (this.value < other.value) { + if (this.regexp.source < other.regexp.source) { return -1; } - if (this.value > other.value) { + if (this.regexp.source > other.regexp.source) { return 1; } return 0; } public equals(other: ContextKeyExpr): boolean { - if (other instanceof ContextKeyGlobExpr) { - return (this.key === other.key && this.value === other.value); + if (other instanceof ContextKeyRegexExpr) { + return (this.key === other.key && this.regexp.source === other.regexp.source); } return false; } public evaluate(context: IContext): boolean { - return match(this.value, context.getValue(this.key)); + return this.regexp.test(context.getValue(this.key)); } public normalize(): ContextKeyExpr { @@ -374,7 +381,7 @@ export class ContextKeyGlobExpr implements ContextKeyExpr { } public serialize(): string { - return this.key + ' =~ \'' + this.value + '\''; + return this.key + ' =~ \'' + this.regexp.source + '\''; } public keys(): string[] { diff --git a/src/vs/platform/contextkey/test/common/contextkey.test.ts b/src/vs/platform/contextkey/test/common/contextkey.test.ts index 4cdda5588c5..9433bf7ca02 100644 --- a/src/vs/platform/contextkey/test/common/contextkey.test.ts +++ b/src/vs/platform/contextkey/test/common/contextkey.test.ts @@ -6,7 +6,6 @@ import * as assert from 'assert'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { match } from 'vs/base/common/glob'; function createContext(ctx: any) { return { @@ -22,8 +21,8 @@ suite('ContextKeyExpr', () => { ContextKeyExpr.has('a1'), ContextKeyExpr.and(ContextKeyExpr.has('and.a')), ContextKeyExpr.has('a2'), - ContextKeyExpr.glob('d3', '**/d*'), - ContextKeyExpr.glob('d4', '**/3*'), + ContextKeyExpr.regex('d3', 'd.*'), + ContextKeyExpr.regex('d4', '\\*\\*/3*'), ContextKeyExpr.equals('b1', 'bb1'), ContextKeyExpr.equals('b2', 'bb2'), ContextKeyExpr.notEquals('c1', 'cc1'), @@ -35,11 +34,11 @@ suite('ContextKeyExpr', () => { ContextKeyExpr.equals('b2', 'bb2'), ContextKeyExpr.notEquals('c1', 'cc1'), ContextKeyExpr.not('d1'), - ContextKeyExpr.glob('d4', '**/3*'), + ContextKeyExpr.regex('d4', '\\*\\*/3*'), ContextKeyExpr.notEquals('c2', 'cc2'), ContextKeyExpr.has('a2'), ContextKeyExpr.equals('b1', 'bb1'), - ContextKeyExpr.glob('d3', '**/d*'), + ContextKeyExpr.regex('d3', 'd.*'), ContextKeyExpr.has('a1'), ContextKeyExpr.and(ContextKeyExpr.equals('and.a', true)), ContextKeyExpr.not('d2') @@ -68,7 +67,7 @@ suite('ContextKeyExpr', () => { 'd': 'd' }); function testExpression(expr: string, expected: boolean): void { - console.log(expr + ' ' + expected); + // console.log(expr + ' ' + expected); let rules = ContextKeyExpr.deserialize(expr); assert.equal(rules.evaluate(context), expected, expr); } @@ -81,7 +80,7 @@ suite('ContextKeyExpr', () => { testExpression(expr + ' == 5', value == '5'); testExpression(expr + ' != 5', value != '5'); testExpression('!' + expr, !value); - testExpression(expr + ' =~ **/d*', match('**/d*', value)); + testExpression(expr + ' =~ d.*', /d.*/.test(value)); } testBatch('a', true); diff --git a/src/vs/platform/extensionManagement/common/extensionManagement.ts b/src/vs/platform/extensionManagement/common/extensionManagement.ts index 9789db5e966..6512e977822 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -86,7 +86,7 @@ export interface IColor { } export interface ILocalization { - languagId: string; + languageId: string; languageName?: string; translations: string; } @@ -229,6 +229,12 @@ export enum StatisticType { Uninstall = 'uninstall' } +export interface IReportedExtension { + id: IExtensionIdentifier; + malicious: boolean; + slow: boolean; +} + export interface IExtensionGalleryService { _serviceBrand: any; isEnabled(): boolean; @@ -240,6 +246,7 @@ export interface IExtensionGalleryService { getChangelog(extension: IGalleryExtension): TPromise; loadCompatibleVersion(extension: IGalleryExtension): TPromise; loadAllDependencies(dependencies: IExtensionIdentifier[]): TPromise; + getExtensionsReport(): TPromise; } export interface InstallExtensionEvent { diff --git a/src/vs/platform/extensionManagement/node/extensionGalleryService.ts b/src/vs/platform/extensionManagement/node/extensionGalleryService.ts index e79adfe2cbc..ae9aa3ca924 100644 --- a/src/vs/platform/extensionManagement/node/extensionGalleryService.ts +++ b/src/vs/platform/extensionManagement/node/extensionGalleryService.ts @@ -9,7 +9,7 @@ import * as path from 'path'; import { TPromise } from 'vs/base/common/winjs.base'; import { distinct } from 'vs/base/common/arrays'; import { getErrorMessage, isPromiseCanceledError } from 'vs/base/common/errors'; -import { StatisticType, IGalleryExtension, IExtensionGalleryService, IGalleryExtensionAsset, IQueryOptions, SortBy, SortOrder, IExtensionManifest, IExtensionIdentifier } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { StatisticType, IGalleryExtension, IExtensionGalleryService, IGalleryExtensionAsset, IQueryOptions, SortBy, SortOrder, IExtensionManifest, IExtensionIdentifier, IReportedExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; import { getGalleryExtensionId, getGalleryExtensionTelemetryData, adoptToGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { assign, getOrDefault } from 'vs/base/common/objects'; import { IRequestService } from 'vs/platform/request/node/request'; @@ -23,6 +23,7 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment' import { readFile } from 'vs/base/node/pfs'; import { writeFileAndFlushSync } from 'vs/base/node/extfs'; import { generateUuid, isUUID } from 'vs/base/common/uuid'; +import { values } from 'vs/base/common/map'; interface IRawGalleryExtensionFile { assetType: string; @@ -309,11 +310,17 @@ function toExtension(galleryExtension: IRawGalleryExtension, extensionsGalleryUr }; } +interface IRawExtensionsReport { + malicious: string[]; + slow: string[]; +} + export class ExtensionGalleryService implements IExtensionGalleryService { _serviceBrand: any; private extensionsGalleryUrl: string; + private extensionsControlUrl: string; private readonly commonHeadersPromise: TPromise<{ [key: string]: string; }>; @@ -324,6 +331,7 @@ export class ExtensionGalleryService implements IExtensionGalleryService { ) { const config = product.extensionsGallery; this.extensionsGalleryUrl = config && config.serviceUrl; + this.extensionsControlUrl = config && config.controlUrl; this.commonHeadersPromise = resolveMarketplaceHeaders(this.environmentService); } @@ -711,6 +719,40 @@ export class ExtensionGalleryService implements IExtensionGalleryService { } return false; } + + getExtensionsReport(): TPromise { + if (!this.isEnabled()) { + return TPromise.wrapError(new Error('No extension gallery service configured.')); + } + + if (!this.extensionsControlUrl) { + return TPromise.as([]); + } + + return this.requestService.request({ type: 'GET', url: this.extensionsControlUrl }).then(context => { + if (context.res.statusCode !== 200) { + return TPromise.wrapError(new Error('Could not get extensions report.')); + } + + return asJson(context).then(result => { + const map = new Map(); + + for (const id of result.malicious) { + const ext = map.get(id) || { id: { id }, malicious: true, slow: false }; + ext.malicious = true; + map.set(id, ext); + } + + for (const id of result.slow) { + const ext = map.get(id) || { id: { id }, malicious: false, slow: true }; + ext.slow = true; + map.set(id, ext); + } + + return TPromise.as(values(map)); + }); + }); + } } export function resolveMarketplaceHeaders(environmentService: IEnvironmentService): TPromise<{ [key: string]: string; }> { diff --git a/src/vs/platform/extensionManagement/test/common/extensionEnablementService.test.ts b/src/vs/platform/extensionManagement/test/common/extensionEnablementService.test.ts index efdb210e498..c4da64ca7cf 100644 --- a/src/vs/platform/extensionManagement/test/common/extensionEnablementService.test.ts +++ b/src/vs/platform/extensionManagement/test/common/extensionEnablementService.test.ts @@ -326,7 +326,7 @@ suite('ExtensionEnablementService Test', () => { }); test('test canChangeEnablement return false for language packs', () => { - assert.equal(testObject.canChangeEnablement(aLocalExtension('pub.a', { localizations: [{ languagId: 'gr', translations: 'somepath' }] })), false); + assert.equal(testObject.canChangeEnablement(aLocalExtension('pub.a', { localizations: [{ languageId: 'gr', translations: 'somepath' }] })), false); }); }); diff --git a/src/vs/platform/node/product.ts b/src/vs/platform/node/product.ts index 00a76d6fc38..ebd1d8e22a4 100644 --- a/src/vs/platform/node/product.ts +++ b/src/vs/platform/node/product.ts @@ -25,6 +25,7 @@ export interface IProductConfiguration { extensionsGallery: { serviceUrl: string; itemUrl: string; + controlUrl: string; }; extensionTips: { [id: string]: string; }; extensionImportantTips: { [id: string]: { name: string; pattern: string; }; }; diff --git a/src/vs/platform/update/electron-main/updateService.darwin.ts b/src/vs/platform/update/electron-main/updateService.darwin.ts index 88b0f193102..25d9cf28beb 100644 --- a/src/vs/platform/update/electron-main/updateService.darwin.ts +++ b/src/vs/platform/update/electron-main/updateService.darwin.ts @@ -36,12 +36,17 @@ export class DarwinUpdateService extends AbstractUpdateService { @ILogService logService: ILogService ) { super(lifecycleService, configurationService, environmentService, logService); - this.onRawError(this.logService.error, this.logService, this.disposables); + this.onRawError(this.onError, this, this.disposables); this.onRawUpdateAvailable(this.onUpdateAvailable, this, this.disposables); this.onRawUpdateDownloaded(this.onUpdateDownloaded, this, this.disposables); this.onRawUpdateNotAvailable(this.onUpdateNotAvailable, this, this.disposables); } + private onError(err: string): void { + this.logService.error('UpdateService error: ', err); + this.setState(State.Idle); + } + protected setUpdateFeedUrl(quality: string): boolean { try { electron.autoUpdater.setFeedURL(createUpdateURL('darwin', quality)); diff --git a/src/vs/platform/workspace/common/workspace.ts b/src/vs/platform/workspace/common/workspace.ts index 1493935a013..af594644bae 100644 --- a/src/vs/platform/workspace/common/workspace.ts +++ b/src/vs/platform/workspace/common/workspace.ts @@ -198,7 +198,7 @@ export class Workspace implements IWorkspace { } public toJSON(): IWorkspace { - return { id: this.id, folders: this.folders, name: this.name, configuration: this.configuration }; + return { id: this.id, folders: this.folders, name: this.name }; } } diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index ed925eadfc5..ed24c1e74ed 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -71,7 +71,6 @@ export interface IWorkspaceData { id: string; name: string; folders: { uri: UriComponents, name: string, index: number }[]; - configuration: UriComponents; } export interface IInitData { diff --git a/src/vs/workbench/api/node/extHostExtensionService.ts b/src/vs/workbench/api/node/extHostExtensionService.ts index 73a8df57d4e..d39a58a432e 100644 --- a/src/vs/workbench/api/node/extHostExtensionService.ts +++ b/src/vs/workbench/api/node/extHostExtensionService.ts @@ -22,7 +22,6 @@ import { Barrier } from 'vs/base/common/async'; import { ILogService } from 'vs/platform/log/common/log'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { ExtHostLogService } from 'vs/workbench/api/node/extHostLogService'; -import URI from 'vs/base/common/uri'; class ExtensionMemento implements IExtensionMemento { @@ -109,7 +108,6 @@ class ExtensionStoragePath { join(storagePath, 'meta.json'), JSON.stringify({ id: this._workspace.id, - configuration: URI.revive(this._workspace.configuration).toString(), name: this._workspace.name }, undefined, 2) ); diff --git a/src/vs/workbench/browser/parts/editor/editor.contribution.ts b/src/vs/workbench/browser/parts/editor/editor.contribution.ts index 0601bf43044..5e459221828 100644 --- a/src/vs/workbench/browser/parts/editor/editor.contribution.ts +++ b/src/vs/workbench/browser/parts/editor/editor.contribution.ts @@ -403,12 +403,12 @@ editorCommands.setup(); // Touch Bar if (isMacintosh) { MenuRegistry.appendMenuItem(MenuId.TouchBarContext, { - command: { id: NavigateBackwardsAction.ID, title: NavigateBackwardsAction.LABEL, iconPath: URI.parse(require.toUrl('vs/workbench/browser/parts/editor/media/back-tb.png')).fsPath }, + command: { id: NavigateBackwardsAction.ID, title: NavigateBackwardsAction.LABEL, iconPath: { dark: URI.parse(require.toUrl('vs/workbench/browser/parts/editor/media/back-tb.png')).fsPath } }, group: 'navigation' }); MenuRegistry.appendMenuItem(MenuId.TouchBarContext, { - command: { id: NavigateForwardAction.ID, title: NavigateForwardAction.LABEL, iconPath: URI.parse(require.toUrl('vs/workbench/browser/parts/editor/media/forward-tb.png')).fsPath }, + command: { id: NavigateForwardAction.ID, title: NavigateForwardAction.LABEL, iconPath: { dark: URI.parse(require.toUrl('vs/workbench/browser/parts/editor/media/forward-tb.png')).fsPath } }, group: 'navigation' }); } diff --git a/src/vs/workbench/browser/parts/editor/editorActions.ts b/src/vs/workbench/browser/parts/editor/editorActions.ts index 81e001915bb..ae87fa3a82a 100644 --- a/src/vs/workbench/browser/parts/editor/editorActions.ts +++ b/src/vs/workbench/browser/parts/editor/editorActions.ts @@ -9,7 +9,7 @@ import nls = require('vs/nls'); import { Action } from 'vs/base/common/actions'; import { mixin } from 'vs/base/common/objects'; import { getCodeEditor } from 'vs/editor/browser/services/codeEditorService'; -import { EditorInput, TextEditorOptions, EditorOptions, IEditorIdentifier, ActiveEditorMoveArguments, ActiveEditorMovePositioning, EditorCommands, ConfirmResult } from 'vs/workbench/common/editor'; +import { EditorInput, TextEditorOptions, EditorOptions, IEditorIdentifier, ActiveEditorMoveArguments, ActiveEditorMovePositioning, EditorCommands, ConfirmResult, IEditorCommandsContext } from 'vs/workbench/common/editor'; import { QuickOpenEntryGroup } from 'vs/base/parts/quickopen/browser/quickOpenModel'; import { EditorQuickOpenEntry, EditorQuickOpenEntryGroup, IEditorQuickOpenEntry, QuickOpenAction } from 'vs/workbench/browser/quickopen'; import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -38,10 +38,11 @@ export class SplitEditorAction extends Action { super(id, label, 'split-editor-action'); } - public run(context?: IEditorIdentifier): TPromise { + public run(context?: IEditorCommandsContext): TPromise { let editorToSplit: IEditor; if (context) { - editorToSplit = this.editorService.getVisibleEditors()[this.editorGroupService.getStacksModel().positionOfGroup(context.group)]; + const stacks = this.editorGroupService.getStacksModel(); + editorToSplit = this.editorService.getVisibleEditors()[stacks.positionOfGroup(stacks.getGroup(context.groupId))]; } else { editorToSplit = this.editorService.getActiveEditor(); } diff --git a/src/vs/workbench/parts/codeEditor/electron-browser/media/codeEditor.css b/src/vs/workbench/parts/codeEditor/electron-browser/media/codeEditor.css deleted file mode 100644 index 443058958ac..00000000000 --- a/src/vs/workbench/parts/codeEditor/electron-browser/media/codeEditor.css +++ /dev/null @@ -1,8 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -.toggle-word-wrap-action { - background: url('WordWrap_16x.svg') center center no-repeat; -} diff --git a/src/vs/workbench/parts/codeEditor/electron-browser/toggleWordWrap.ts b/src/vs/workbench/parts/codeEditor/electron-browser/toggleWordWrap.ts index 39b0436ad9e..4925f696c31 100644 --- a/src/vs/workbench/parts/codeEditor/electron-browser/toggleWordWrap.ts +++ b/src/vs/workbench/parts/codeEditor/electron-browser/toggleWordWrap.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import 'vs/css!./media/codeEditor'; import * as nls from 'vs/nls'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; @@ -259,7 +258,7 @@ MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: 'editor.action.toggleWordWrap', title: nls.localize('unwrapMinified', "Disable wrapping for this file"), - iconClass: 'toggle-word-wrap-action' + iconPath: { dark: URI.parse(require.toUrl('vs/workbench/parts/codeEditor/electron-browser/WordWrap_16x.svg')).fsPath } }, group: 'navigation', order: 1, @@ -273,7 +272,7 @@ MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: 'editor.action.toggleWordWrap', title: nls.localize('wrapMinified', "Enable wrapping for this file"), - iconClass: 'toggle-word-wrap-action' + iconPath: { dark: URI.parse(require.toUrl('vs/workbench/parts/codeEditor/electron-browser/WordWrap_16x.svg')).fsPath } }, group: 'navigation', order: 1, diff --git a/src/vs/workbench/parts/debug/browser/debugActionItems.ts b/src/vs/workbench/parts/debug/browser/debugActionItems.ts index 12887c33f44..2f8e4a05dfa 100644 --- a/src/vs/workbench/parts/debug/browser/debugActionItems.ts +++ b/src/vs/workbench/parts/debug/browser/debugActionItems.ts @@ -158,7 +158,7 @@ export class StartDebugActionItem implements IActionItem { if (name === manager.selectedName && launch === manager.selectedLaunch) { this.selected = this.options.length; } - const label = launches.length > 1 ? `${name} (${launch.workspace.name})` : name; + const label = launches.length > 1 ? `${name} (${launch.name})` : name; this.options.push({ label, handler: () => { manager.selectConfiguration(launch, name); return true; } }); })); @@ -169,10 +169,10 @@ export class StartDebugActionItem implements IActionItem { const disabledIdx = this.options.length - 1; launches.forEach(l => { - const label = launches.length > 1 ? nls.localize("addConfigTo", "Add Config ({0})...", l.workspace.name) : nls.localize('addConfiguration', "Add Configuration..."); + const label = launches.length > 1 ? nls.localize("addConfigTo", "Add Config ({0})...", l.name) : nls.localize('addConfiguration', "Add Configuration..."); this.options.push({ label, handler: () => { - this.commandService.executeCommand('debug.addConfiguration', l.workspace.uri.toString()).done(undefined, errors.onUnexpectedError); + this.commandService.executeCommand('debug.addConfiguration', l.uri.toString()).done(undefined, errors.onUnexpectedError); return false; } }); diff --git a/src/vs/workbench/parts/debug/browser/debugActions.ts b/src/vs/workbench/parts/debug/browser/debugActions.ts index e98c1e52ab3..293425c4a9f 100644 --- a/src/vs/workbench/parts/debug/browser/debugActions.ts +++ b/src/vs/workbench/parts/debug/browser/debugActions.ts @@ -142,7 +142,7 @@ export class StartAction extends AbstractDebugAction { if (contextService && contextService.getWorkbenchState() === WorkbenchState.EMPTY && processes.length > 0) { return false; } - if (processes.some(p => p.getName(false) === configName && (!launch || p.session.root.uri.toString() === launch.workspace.uri.toString()))) { + if (processes.some(p => p.getName(false) === configName && (!launch || !launch.workspace || !p.session.root || p.session.root.uri.toString() === launch.workspace.uri.toString()))) { return false; } const compound = launch && launch.getCompound(configName); diff --git a/src/vs/workbench/parts/debug/browser/debugQuickOpen.ts b/src/vs/workbench/parts/debug/browser/debugQuickOpen.ts index cd90a6fd6fc..4b93cc51109 100644 --- a/src/vs/workbench/parts/debug/browser/debugQuickOpen.ts +++ b/src/vs/workbench/parts/debug/browser/debugQuickOpen.ts @@ -29,7 +29,7 @@ class AddConfigEntry extends Model.QuickOpenEntry { } public getDescription(): string { - return this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE ? this.launch.workspace.name : ''; + return this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE ? this.launch.name : ''; } public getAriaLabel(): string { @@ -40,7 +40,7 @@ class AddConfigEntry extends Model.QuickOpenEntry { if (mode === QuickOpen.Mode.PREVIEW) { return false; } - this.commandService.executeCommand('debug.addConfiguration', this.launch.workspace.uri.toString()).done(undefined, errors.onUnexpectedError); + this.commandService.executeCommand('debug.addConfiguration', this.launch.uri.toString()).done(undefined, errors.onUnexpectedError); return true; } @@ -57,7 +57,7 @@ class StartDebugEntry extends Model.QuickOpenEntry { } public getDescription(): string { - return this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE ? this.launch.workspace.name : ''; + return this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE ? this.launch.name : ''; } public getAriaLabel(): string { @@ -110,7 +110,7 @@ export class DebugQuickOpenHandler extends Quickopen.QuickOpenHandler { }); } launches.forEach((l, index) => { - const label = launches.length > 1 ? nls.localize("addConfigTo", "Add Config ({0})...", l.workspace.name) : nls.localize('addConfiguration', "Add Configuration..."); + const label = launches.length > 1 ? nls.localize("addConfigTo", "Add Config ({0})...", l.name) : nls.localize('addConfiguration', "Add Configuration..."); const entry = new AddConfigEntry(label, l, this.commandService, this.contextService, Filters.matchesContiguousSubString(input, label)); if (index === 0) { configurations.push(new QuickOpenEntryGroup(entry, undefined, true)); diff --git a/src/vs/workbench/parts/debug/browser/debugStatus.ts b/src/vs/workbench/parts/debug/browser/debugStatus.ts index da4007a0a80..01e55d619ed 100644 --- a/src/vs/workbench/parts/debug/browser/debugStatus.ts +++ b/src/vs/workbench/parts/debug/browser/debugStatus.ts @@ -95,7 +95,7 @@ export class DebugStatus extends Themable implements IStatusbarItem { if (manager.selectedName) { const name = manager.selectedName; this.statusBarItem.style.display = 'block'; - this.label.textContent = manager.getLaunches().length > 1 ? `${name} (${manager.selectedLaunch.workspace.name})` : name; + this.label.textContent = manager.getLaunches().length > 1 ? `${name} (${manager.selectedLaunch.name})` : name; } else { this.statusBarItem.style.display = 'none'; } diff --git a/src/vs/workbench/parts/debug/common/debug.ts b/src/vs/workbench/parts/debug/common/debug.ts index 43d81946f37..8356f23b448 100644 --- a/src/vs/workbench/parts/debug/common/debug.ts +++ b/src/vs/workbench/parts/debug/common/debug.ts @@ -437,6 +437,11 @@ export interface ILaunch { */ uri: uri; + /** + * Name of the launch. + */ + name: string; + workspace: IWorkspaceFolder; /** @@ -466,7 +471,7 @@ export interface ILaunch { /** * Opens the launch.json file. Creates if it does not exist. */ - openConfigFile(sideBySide: boolean, type?: string): TPromise<{ editor: IEditor; configFileCreated: boolean; }>; + openConfigFile(sideBySide: boolean, type?: string): TPromise; } // Debug service interfaces diff --git a/src/vs/workbench/parts/debug/common/debugModel.ts b/src/vs/workbench/parts/debug/common/debugModel.ts index 2923456a42c..d585e6276d8 100644 --- a/src/vs/workbench/parts/debug/common/debugModel.ts +++ b/src/vs/workbench/parts/debug/common/debugModel.ts @@ -545,7 +545,7 @@ export class Process implements IProcess { } public getName(includeRoot: boolean): string { - return includeRoot ? `${this.configuration.name} (${resources.basenameOrAuthority(this.session.root.uri)})` : this.configuration.name; + return includeRoot && this.session.root ? `${this.configuration.name} (${resources.basenameOrAuthority(this.session.root.uri)})` : this.configuration.name; } public get state(): ProcessState { diff --git a/src/vs/workbench/parts/debug/electron-browser/debug.contribution.ts b/src/vs/workbench/parts/debug/electron-browser/debug.contribution.ts index f8768ff718e..fb9e5dec2ed 100644 --- a/src/vs/workbench/parts/debug/electron-browser/debug.contribution.ts +++ b/src/vs/workbench/parts/debug/electron-browser/debug.contribution.ts @@ -223,7 +223,7 @@ if (isMacintosh) { const registerTouchBarEntry = (id: string, title: string, order, when: ContextKeyExpr, icon: string) => { MenuRegistry.appendMenuItem(MenuId.TouchBarContext, { command: { - id, title, iconPath: URI.parse(require.toUrl(`vs/workbench/parts/debug/electron-browser/media/${icon}`)).fsPath + id, title, iconPath: { dark: URI.parse(require.toUrl(`vs/workbench/parts/debug/electron-browser/media/${icon}`)).fsPath } }, when, group: '9_debug', diff --git a/src/vs/workbench/parts/debug/electron-browser/debugCommands.ts b/src/vs/workbench/parts/debug/electron-browser/debugCommands.ts index af425b59ffb..3387c35f8a5 100644 --- a/src/vs/workbench/parts/debug/electron-browser/debugCommands.ts +++ b/src/vs/workbench/parts/debug/electron-browser/debugCommands.ts @@ -163,17 +163,17 @@ export function registerCommands(): void { weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), when: undefined, primary: undefined, - handler: (accessor, workspaceUri: string) => { + handler: (accessor, launchUri: string) => { const manager = accessor.get(IDebugService).getConfigurationManager(); if (accessor.get(IWorkspaceContextService).getWorkbenchState() === WorkbenchState.EMPTY) { accessor.get(IMessageService).show(severity.Info, nls.localize('noFolderDebugConfig', "Please first open a folder in order to do advanced debug configuration.")); return TPromise.as(null); } - const launch = manager.getLaunches().filter(l => l.workspace.uri.toString() === workspaceUri).pop() || manager.selectedLaunch; + const launch = manager.getLaunches().filter(l => l.uri.toString() === launchUri).pop() || manager.selectedLaunch; - return launch.openConfigFile(false).done(result => { - if (result.editor && !result.configFileCreated) { - const codeEditor = result.editor.getControl(); + return launch.openConfigFile(false).done(editor => { + if (editor) { + const codeEditor = editor.getControl(); if (codeEditor) { return codeEditor.getContribution(EDITOR_CONTRIBUTION_ID).addLaunchConfiguration(); } diff --git a/src/vs/workbench/parts/debug/electron-browser/debugConfigurationManager.ts b/src/vs/workbench/parts/debug/electron-browser/debugConfigurationManager.ts index f31e9cb9b68..c7351052731 100644 --- a/src/vs/workbench/parts/debug/electron-browser/debugConfigurationManager.ts +++ b/src/vs/workbench/parts/debug/electron-browser/debugConfigurationManager.ts @@ -9,6 +9,7 @@ import Event, { Emitter } from 'vs/base/common/event'; import { TPromise } from 'vs/base/common/winjs.base'; import * as strings from 'vs/base/common/strings'; import { first } from 'vs/base/common/arrays'; +import severity from 'vs/base/common/severity'; import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; import * as objects from 'vs/base/common/objects'; import uri from 'vs/base/common/uri'; @@ -24,7 +25,7 @@ import { IExtensionService } from 'vs/platform/extensions/common/extensions'; import { IJSONContributionRegistry, Extensions as JSONExtensions } from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IFileService } from 'vs/platform/files/common/files'; -import { IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; +import { IWorkspaceContextService, IWorkspaceFolder, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IDebugConfigurationProvider, IRawAdapter, ICompound, IDebugConfiguration, IConfig, IEnvConfig, IGlobalConfig, IConfigurationManager, ILaunch } from 'vs/workbench/parts/debug/common/debug'; @@ -33,6 +34,8 @@ import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/edi import { IQuickOpenService } from 'vs/platform/quickOpen/common/quickOpen'; import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; +import { launchSchemaId } from 'vs/workbench/services/configuration/common/configuration'; +import { IMessageService } from 'vs/platform/message/common/message'; // debuggers extension point export const debuggersExtPoint = extensionsRegistry.ExtensionsRegistry.registerExtensionPoint('debuggers', [], { @@ -147,14 +150,12 @@ const breakpointsExtPoint = extensionsRegistry.ExtensionsRegistry.registerExtens }); // debug general schema - -export const schemaId = 'vscode://schemas/launch'; const defaultCompound: ICompound = { name: 'Compound', configurations: [] }; const schema: IJSONSchema = { - id: schemaId, + id: launchSchemaId, type: 'object', title: nls.localize('app.launch.json.title', "Launch"), - required: ['version', 'configurations'], + required: [], default: { version: '0.2.0', configurations: [], compounds: [] }, properties: { version: { @@ -201,7 +202,7 @@ const schema: IJSONSchema = { }; const jsonRegistry = Registry.as(JSONExtensions.JSONContribution); -jsonRegistry.registerSchema(schemaId, schema); +jsonRegistry.registerSchema(launchSchemaId, schema); const DEBUG_SELECTED_CONFIG_NAME_KEY = 'debug.selectedconfigname'; const DEBUG_SELECTED_ROOT = 'debug.selectedroot'; @@ -232,7 +233,7 @@ export class ConfigurationManager implements IConfigurationManager { this.registerListeners(lifecycleService); this.initLaunches(); const previousSelectedRoot = this.storageService.get(DEBUG_SELECTED_ROOT, StorageScope.WORKSPACE); - const filtered = this.launches.filter(l => l.workspace.uri.toString() === previousSelectedRoot); + const filtered = this.launches.filter(l => l.uri.toString() === previousSelectedRoot); this.selectConfiguration(filtered.length ? filtered[0] : undefined, this.storageService.get(DEBUG_SELECTED_CONFIG_NAME_KEY, StorageScope.WORKSPACE)); } @@ -335,6 +336,10 @@ export class ConfigurationManager implements IConfigurationManager { private initLaunches(): void { this.launches = this.contextService.getWorkspace().folders.map(folder => this.instantiationService.createInstance(Launch, this, folder)); + if (this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE) { + this.launches.push(this.instantiationService.createInstance(WorkspaceLaunch, this)); + } + if (this.launches.indexOf(this._selectedLaunch) === -1) { this._selectedLaunch = undefined; } @@ -356,6 +361,14 @@ export class ConfigurationManager implements IConfigurationManager { return this._onDidSelectConfigurationName.event; } + public getWorkspaceLaunch(): ILaunch { + if (this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE) { + return this.launches[this.launches.length - 1]; + } + + return undefined; + } + public selectConfiguration(launch?: ILaunch, name?: string, debugStarted?: boolean): void { const previousLaunch = this._selectedLaunch; const previousName = this._selectedName; @@ -438,7 +451,7 @@ export class ConfigurationManager implements IConfigurationManager { private store(): void { this.storageService.store(DEBUG_SELECTED_CONFIG_NAME_KEY, this.selectedName, StorageScope.WORKSPACE); if (this._selectedLaunch) { - this.storageService.store(DEBUG_SELECTED_ROOT, this._selectedLaunch.workspace.uri.toString(), StorageScope.WORKSPACE); + this.storageService.store(DEBUG_SELECTED_ROOT, this._selectedLaunch.uri.toString(), StorageScope.WORKSPACE); } } @@ -453,15 +466,28 @@ class Launch implements ILaunch { private configurationManager: ConfigurationManager, public workspace: IWorkspaceFolder, @IFileService private fileService: IFileService, - @IWorkbenchEditorService private editorService: IWorkbenchEditorService, - @IConfigurationService private configurationService: IConfigurationService, - @IConfigurationResolverService private configurationResolverService: IConfigurationResolverService + @IWorkbenchEditorService protected editorService: IWorkbenchEditorService, + @IConfigurationService protected configurationService: IConfigurationService, + @IConfigurationResolverService private configurationResolverService: IConfigurationResolverService, + @IMessageService private messageService: IMessageService ) { // noop } + public get uri(): uri { + return this.workspace.uri.with({ path: paths.join(this.workspace.uri.path, '/.vscode/launch.json') }); + } + + public get name(): string { + return this.workspace.name; + } + + protected getConfig(): IGlobalConfig { + return this.configurationService.getValue('launch', { resource: this.workspace.uri }); + } + public getCompound(name: string): ICompound { - const config = this.configurationService.getValue('launch', { resource: this.workspace.uri }); + const config = this.getConfig(); if (!config || !config.compounds) { return null; } @@ -470,7 +496,7 @@ class Launch implements ILaunch { } public getConfigurationNames(): string[] { - const config = this.configurationService.getValue('launch', { resource: this.workspace.uri }); + const config = this.getConfig(); if (!config || !config.configurations || !Array.isArray(config.configurations)) { return []; } else { @@ -487,7 +513,7 @@ class Launch implements ILaunch { } public getConfiguration(name: string): IConfig { - const config = objects.deepClone(this.configurationService.getValue('launch', { resource: this.workspace.uri })); + const config = this.getConfig(); if (!config || !config.configurations) { return null; } @@ -518,15 +544,11 @@ class Launch implements ILaunch { return this.configurationResolverService.resolveInteractiveVariables(result, adapter ? adapter.variables : null); } - public get uri(): uri { - return this.workspace.uri.with({ path: paths.join(this.workspace.uri.path, '/.vscode/launch.json') }); - } - - public openConfigFile(sideBySide: boolean, type?: string): TPromise<{ editor: IEditor; configFileCreated: boolean; }> { + public openConfigFile(sideBySide: boolean, type?: string): TPromise { const resource = this.uri; let configFileCreated = false; - return this.fileService.resolveContent(resource).then(content => content, err => { + return this.fileService.resolveContent(resource).then(content => content.value, err => { // launch.json not found: create one by collecting launch configs from debugConfigProviders @@ -547,17 +569,17 @@ class Launch implements ILaunch { configFileCreated = true; return this.fileService.updateContent(resource, content).then(() => { // convert string into IContent; see #32135 - return { value: content }; + return content; }); }); }).then(content => { if (!content) { - return { editor: undefined, configFileCreated }; + return undefined; } - const index = content.value.indexOf(`"${this.configurationManager.selectedName}"`); + const index = content.indexOf(`"${this.configurationManager.selectedName}"`); let startLineNumber = 1; for (let i = 0; i < index; i++) { - if (content.value.charAt(i) === '\n') { + if (content.charAt(i) === '\n') { startLineNumber++; } } @@ -571,9 +593,46 @@ class Launch implements ILaunch { pinned: configFileCreated, // pin only if config file is created #8727 revealIfVisible: true }, - }, sideBySide).then(editor => ({ editor, configFileCreated })); + }, sideBySide).then(editor => { + if (configFileCreated) { + this.messageService.show(severity.Info, nls.localize('NewLaunchConfig', "Please set up the launch configuration file for your application.")); + } + + return editor; + }); }, (error) => { throw new Error(nls.localize('DebugConfig.failed', "Unable to create 'launch.json' file inside the '.vscode' folder ({0}).", error)); }); } } + +class WorkspaceLaunch extends Launch implements ILaunch { + + constructor( + configurationManager: ConfigurationManager, + @IFileService fileService: IFileService, + @IWorkbenchEditorService editorService: IWorkbenchEditorService, + @IConfigurationService configurationService: IConfigurationService, + @IConfigurationResolverService configurationResolverService: IConfigurationResolverService, + @IWorkspaceContextService private workspaceContextService: IWorkspaceContextService, + @IMessageService messageService: IMessageService + ) { + super(configurationManager, undefined, fileService, editorService, configurationService, configurationResolverService, messageService); + } + + get uri(): uri { + return this.workspaceContextService.getWorkspace().configuration; + } + + get name(): string { + return nls.localize('workspace', "workspace"); + } + + protected getConfig(): IGlobalConfig { + return this.configurationService.inspect('launch').workspace; + } + + openConfigFile(sideBySide: boolean, type?: string): TPromise { + return this.editorService.openEditor({ resource: this.workspaceContextService.getWorkspace().configuration }); + } +} diff --git a/src/vs/workbench/parts/debug/electron-browser/debugService.ts b/src/vs/workbench/parts/debug/electron-browser/debugService.ts index b2434227ac5..b42ea9956db 100644 --- a/src/vs/workbench/parts/debug/electron-browser/debugService.ts +++ b/src/vs/workbench/parts/debug/electron-browser/debugService.ts @@ -53,6 +53,7 @@ import { IBroadcastService, IBroadcast } from 'vs/platform/broadcast/electron-br import { IRemoteConsoleLog, parse, getFirstFrame } from 'vs/base/node/console'; import { Source } from 'vs/workbench/parts/debug/common/debugSource'; import { TaskEvent, TaskEventKind } from 'vs/workbench/parts/tasks/common/tasks'; +import { sequence } from 'vs/base/common/async'; const DEBUG_BREAKPOINTS_KEY = 'debug.breakpoint'; const DEBUG_BREAKPOINTS_ACTIVATED_KEY = 'debug.breakpointactivated'; @@ -668,8 +669,8 @@ export class DebugService implements debug.IDebugService { this.model.getBreakpoints().forEach(bp => bp.verified = false); } this.launchJsonChanged = false; - const manager = this.getConfigurationManager(); - const launch = root ? manager.getLaunches().filter(l => l.workspace.uri.toString() === root.uri.toString()).pop() : undefined; + const launch = root ? this.configurationManager.getLaunches().filter(l => l.workspace && l.workspace.uri.toString() === root.uri.toString()).pop() + : this.configurationManager.getWorkspaceLaunch(); let config: debug.IConfig, compound: debug.ICompound; if (!configOrName) { @@ -683,7 +684,7 @@ export class DebugService implements debug.IDebugService { } if (launch) { // in the drop down the name of the top most compound takes precedence over the launch config name - manager.selectConfiguration(launch, topCompoundName || (typeof configOrName === 'string' ? configOrName : undefined), true); + this.configurationManager.selectConfiguration(launch, topCompoundName || (typeof configOrName === 'string' ? configOrName : undefined), true); } if (compound) { @@ -692,7 +693,22 @@ export class DebugService implements debug.IDebugService { "Compound must have \"configurations\" attribute set in order to start multiple configurations."))); } - return TPromise.join(compound.configurations.map(name => name !== compound.name ? this.startDebugging(root, name, noDebug, topCompoundName || compound.name) : TPromise.as(null))); + return sequence(compound.configurations.map(name => () => { + if (name === compound.name) { + return TPromise.as(null); + } + + let rootForName = root; + if (launch === this.configurationManager.getWorkspaceLaunch()) { + // For workspace launches allow comound referencing configurations across folder + const launchContainingName = this.configurationManager.getLaunches().filter(l => !!l.getConfiguration(name)).pop(); + if (launchContainingName) { + rootForName = launchContainingName.workspace; + } + } + + return this.startDebugging(rootForName, name, noDebug, topCompoundName || compound.name); + })); } if (configOrName && !config) { const message = !!launch ? nls.localize('configMissing', "Configuration '{0}' is missing in 'launch.json'.", configOrName) : @@ -723,7 +739,7 @@ export class DebugService implements debug.IDebugService { return (type ? TPromise.as(null) : this.configurationManager.guessAdapter().then(a => type = a && a.type)).then(() => (type ? this.extensionService.activateByEvent(`onDebugResolve:${type}`) : TPromise.as(null)).then(() => - this.configurationManager.resolveConfigurationByProviders(launch ? launch.workspace.uri : undefined, type, config).then(config => { + this.configurationManager.resolveConfigurationByProviders(launch && launch.workspace ? launch.workspace.uri : undefined, type, config).then(config => { // a falsy config indicates an aborted launch if (config && config.type) { return this.createProcess(root, config, sessionId); @@ -803,12 +819,7 @@ export class DebugService implements debug.IDebugService { return undefined; } - return this.configurationManager.selectedLaunch.openConfigFile(false).then(result => { - if (result.configFileCreated) { - this.messageService.show(severity.Info, nls.localize('NewLaunchConfig', "Please set up the launch configuration file for your application. {0}", err.message)); - } - return undefined; - }); + return this.configurationManager.selectedLaunch.openConfigFile(false).then(editor => void 0); }) ); } @@ -1025,7 +1036,7 @@ export class DebugService implements debug.IDebugService { const preserveFocus = focusedProcess && process.getId() === focusedProcess.getId(); return process.session.disconnect(true).then(() => { - if (strings.equalsIgnoreCase(process.configuration.type, 'extensionHost')) { + if (strings.equalsIgnoreCase(process.configuration.type, 'extensionHost') && process.session.root) { return this.broadcastService.broadcast({ channel: EXTENSION_RELOAD_BROADCAST_CHANNEL, payload: [process.session.root.uri.fsPath] diff --git a/src/vs/workbench/parts/extensions/browser/extensionEditor.ts b/src/vs/workbench/parts/extensions/browser/extensionEditor.ts index 94917051daf..84e9f9c800e 100644 --- a/src/vs/workbench/parts/extensions/browser/extensionEditor.ts +++ b/src/vs/workbench/parts/extensions/browser/extensionEditor.ts @@ -465,7 +465,8 @@ export class ExtensionEditor extends BaseEditor { this.renderColors(content, manifest, layout), this.renderJSONValidation(content, manifest, layout), this.renderDebuggers(content, manifest, layout), - this.renderViews(content, manifest, layout) + this.renderViews(content, manifest, layout), + this.renderLocalizations(content, manifest, layout) ]; const isEmpty = !renders.reduce((v, r) => r || v, false); @@ -621,6 +622,26 @@ export class ExtensionEditor extends BaseEditor { return true; } + private renderLocalizations(container: HTMLElement, manifest: IExtensionManifest, onDetailsToggle: Function): boolean { + const contributes = manifest.contributes; + const localizations = contributes && contributes.localizations || []; + + if (!localizations.length) { + return false; + } + + const details = $('details', { open: true, ontoggle: onDetailsToggle }, + $('summary', null, localize('localizations', "Localizations ({0})", localizations.length)), + $('table', null, + $('tr', null, $('th', null, localize('localizations language id', "Language Id")), $('th', null, localize('localizations language name', "Langauge Name")), $('th', null, localize('translations location', "Translations Location"))), + ...localizations.map(localization => $('tr', null, $('td', null, localization.languageId), $('td', null, localization.languageName), $('td', null, localization.translations))) + ) + ); + + append(container, details); + return true; + } + private renderColorThemes(container: HTMLElement, manifest: IExtensionManifest, onDetailsToggle: Function): boolean { const contributes = manifest.contributes; const contrib = contributes && contributes.themes || []; diff --git a/src/vs/workbench/parts/files/electron-browser/fileActions.contribution.ts b/src/vs/workbench/parts/files/electron-browser/fileActions.contribution.ts index 0ef23baac4c..f365329c68a 100644 --- a/src/vs/workbench/parts/files/electron-browser/fileActions.contribution.ts +++ b/src/vs/workbench/parts/files/electron-browser/fileActions.contribution.ts @@ -23,7 +23,7 @@ import { OPEN_FOLDER_SETTINGS_COMMAND, OPEN_FOLDER_SETTINGS_LABEL } from 'vs/wor import { AutoSaveContext } from 'vs/workbench/services/textfile/common/textfiles'; import { ResourceContextKey } from 'vs/workbench/common/resources'; import { WorkbenchListDoubleSelection } from 'vs/platform/list/browser/listService'; - +import URI from 'vs/base/common/uri'; // Contribute Global Actions const category = nls.localize('filesCategory', "File"); @@ -116,17 +116,23 @@ function appendEditorTitleContextMenuItem(id: string, title: string, when: Conte } // Editor Title Menu for Conflict Resolution -appendSaveConflictEditorTitleAction('workbench.files.action.acceptLocalChanges', nls.localize('acceptLocalChanges', "Use your changes and overwrite disk contents"), 'save-conflict-action-accept-changes', -10, acceptLocalChangesCommand); -appendSaveConflictEditorTitleAction('workbench.files.action.revertLocalChanges', nls.localize('revertLocalChanges', "Discard your changes and revert to content on disk"), 'save-conflict-action-revert-changes', -9, revertLocalChangesCommand); +appendSaveConflictEditorTitleAction('workbench.files.action.acceptLocalChanges', nls.localize('acceptLocalChanges', "Use your changes and overwrite disk contents"), { + light: URI.parse(require.toUrl(`vs/workbench/parts/files/electron-browser/media/check.svg`)).fsPath, + dark: URI.parse(require.toUrl(`vs/workbench/parts/files/electron-browser/media/check-inverse.svg`)).fsPath +}, -10, acceptLocalChangesCommand); +appendSaveConflictEditorTitleAction('workbench.files.action.revertLocalChanges', nls.localize('revertLocalChanges', "Discard your changes and revert to content on disk"), { + light: URI.parse(require.toUrl(`vs/workbench/parts/files/electron-browser/media/undo.svg`)).fsPath, + dark: URI.parse(require.toUrl(`vs/workbench/parts/files/electron-browser/media/undo-inverse.svg`)).fsPath +}, -9, revertLocalChangesCommand); -function appendSaveConflictEditorTitleAction(id: string, title: string, iconClass: string, order: number, command: ICommandHandler): void { +function appendSaveConflictEditorTitleAction(id: string, title: string, iconPath: { dark: string; light?: string; }, order: number, command: ICommandHandler): void { // Command CommandsRegistry.registerCommand(id, command); // Action MenuRegistry.appendMenuItem(MenuId.EditorTitle, { - command: { id, title, iconClass }, + command: { id, title, iconPath }, when: ContextKeyExpr.equals(CONFLICT_RESOLUTION_CONTEXT, true), group: 'navigation', order diff --git a/src/vs/workbench/parts/files/electron-browser/media/fileactions.css b/src/vs/workbench/parts/files/electron-browser/media/fileactions.css index a073640bc8f..2eb68b32b03 100644 --- a/src/vs/workbench/parts/files/electron-browser/media/fileactions.css +++ b/src/vs/workbench/parts/files/electron-browser/media/fileactions.css @@ -75,22 +75,6 @@ background-image: url('split-editor-horizontal-inverse.svg'); } -.monaco-workbench .save-conflict-action-accept-changes { - background: url('check.svg') center center no-repeat; -} - -.vs-dark .monaco-workbench .save-conflict-action-accept-changes { - background: url('check-inverse.svg') center center no-repeat; -} - -.monaco-workbench .save-conflict-action-revert-changes { - background: url('undo.svg') center center no-repeat; -} - -.vs-dark .monaco-workbench .save-conflict-action-revert-changes { - background: url('undo-inverse.svg') center center no-repeat; -} - .monaco-workbench .file-editor-action.action-open-preview { background: url('Preview.svg') center center no-repeat; } diff --git a/src/vs/workbench/services/configuration/common/configuration.ts b/src/vs/workbench/services/configuration/common/configuration.ts index 4ca66967670..3bdae770e7a 100644 --- a/src/vs/workbench/services/configuration/common/configuration.ts +++ b/src/vs/workbench/services/configuration/common/configuration.ts @@ -23,10 +23,11 @@ export const defaultSettingsSchemaId = 'vscode://schemas/settings/default'; export const userSettingsSchemaId = 'vscode://schemas/settings/user'; export const workspaceSettingsSchemaId = 'vscode://schemas/settings/workspace'; export const folderSettingsSchemaId = 'vscode://schemas/settings/folder'; +export const launchSchemaId = 'vscode://schemas/launch'; export const TASKS_CONFIGURATION_KEY = 'tasks'; export const LAUNCH_CONFIGURATION_KEY = 'launch'; export const WORKSPACE_STANDALONE_CONFIGURATIONS = Object.create(null); WORKSPACE_STANDALONE_CONFIGURATIONS[TASKS_CONFIGURATION_KEY] = `${FOLDER_CONFIG_FOLDER_NAME}/tasks.json`; -WORKSPACE_STANDALONE_CONFIGURATIONS[LAUNCH_CONFIGURATION_KEY] = `${FOLDER_CONFIG_FOLDER_NAME}/launch.json`; \ No newline at end of file +WORKSPACE_STANDALONE_CONFIGURATIONS[LAUNCH_CONFIGURATION_KEY] = `${FOLDER_CONFIG_FOLDER_NAME}/launch.json`; diff --git a/src/vs/workbench/services/configuration/common/configurationExtensionPoint.ts b/src/vs/workbench/services/configuration/common/configurationExtensionPoint.ts index 2be9fac95bd..a15de28318b 100644 --- a/src/vs/workbench/services/configuration/common/configurationExtensionPoint.ts +++ b/src/vs/workbench/services/configuration/common/configurationExtensionPoint.ts @@ -10,7 +10,7 @@ import { IJSONSchema } from 'vs/base/common/jsonSchema'; import { ExtensionsRegistry, IExtensionPointUser } from 'vs/platform/extensions/common/extensionsRegistry'; 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 } from 'vs/workbench/services/configuration/common/configuration'; +import { workspaceSettingsSchemaId, launchSchemaId } from 'vs/workbench/services/configuration/common/configuration'; const configurationRegistry = Registry.as(Extensions.Configuration); @@ -201,6 +201,12 @@ jsonRegistry.registerSchema('vscode://schemas/workspaceConfig', { description: nls.localize('workspaceConfig.settings.description', "Workspace settings"), $ref: workspaceSettingsSchemaId }, + 'launch': { + type: 'object', + default: { configurations: [], compounds: [] }, + description: nls.localize('workspaceConfig.launch.description', "Workspace launch configurations"), + $ref: launchSchemaId + }, 'extensions': { type: 'object', default: {}, diff --git a/src/vs/workbench/services/configuration/common/configurationModels.ts b/src/vs/workbench/services/configuration/common/configurationModels.ts index 2d9b9a6bd33..27be593edfe 100644 --- a/src/vs/workbench/services/configuration/common/configurationModels.ts +++ b/src/vs/workbench/services/configuration/common/configurationModels.ts @@ -14,7 +14,7 @@ import { Workspace } from 'vs/platform/workspace/common/workspace'; import { StrictResourceMap } from 'vs/base/common/map'; import URI from 'vs/base/common/uri'; -export class WorkspaceSettingsModel extends ConfigurationModel { +export class SettingsModel extends ConfigurationModel { private _unsupportedKeys: string[]; @@ -32,30 +32,49 @@ export class WorkspaceSettingsModel extends ConfigurationModel { export class WorkspaceConfigurationModelParser extends ConfigurationModelParser { private _folders: IStoredWorkspaceFolder[] = []; - private _workspaceSettingsModelParser: FolderSettingsModelParser; + private _settingsModelParser: FolderSettingsModelParser; + private _launchModel: ConfigurationModel; constructor(name: string) { super(name); - this._workspaceSettingsModelParser = new FolderSettingsModelParser(name); + this._settingsModelParser = new FolderSettingsModelParser(name); + this._launchModel = new ConfigurationModel(); } get folders(): IStoredWorkspaceFolder[] { return this._folders; } - get workspaceSettingsModel(): WorkspaceSettingsModel { - return this._workspaceSettingsModelParser.folderSettingsModel; + get settingsModel(): SettingsModel { + return this._settingsModelParser.settingsModel; + } + + get launchModel(): ConfigurationModel { + return this._launchModel; } reprocessWorkspaceSettings(): void { - this._workspaceSettingsModelParser.reprocess(); + this._settingsModelParser.reprocess(); } protected parseRaw(raw: any): IConfigurationModel { this._folders = (raw['folders'] || []) as IStoredWorkspaceFolder[]; - this._workspaceSettingsModelParser.parse(raw['settings']); + this._settingsModelParser.parse(raw['settings']); + this._launchModel = this.createConfigurationModelFrom(raw, 'launch'); return super.parseRaw(raw); } + + private createConfigurationModelFrom(raw: any, key: string): ConfigurationModel { + const data = raw[key]; + if (data) { + const contents = toValuesTree(data, message => console.error(`Conflict in settings file ${this._name}: ${message}`)); + const scopedContents = Object.create(null); + scopedContents[key] = contents; + const keys = Object.keys(data).map(k => `${key}.${k}`); + return new ConfigurationModel(scopedContents, keys, []); + } + return new ConfigurationModel(); + } } export class StandaloneConfigurationModelParser extends ConfigurationModelParser { @@ -77,7 +96,7 @@ export class StandaloneConfigurationModelParser extends ConfigurationModelParser export class FolderSettingsModelParser extends ConfigurationModelParser { private _raw: any; - private _workspaceSettingsModel: WorkspaceSettingsModel; + private _settingsModel: SettingsModel; constructor(name: string, private configurationScope?: ConfigurationScope) { super(name); @@ -89,11 +108,11 @@ export class FolderSettingsModelParser extends ConfigurationModelParser { } get configurationModel(): ConfigurationModel { - return this._workspaceSettingsModel || new WorkspaceSettingsModel({}, [], [], []); + return this._settingsModel || new SettingsModel({}, [], [], []); } - get folderSettingsModel(): WorkspaceSettingsModel { - return this.configurationModel; + get settingsModel(): SettingsModel { + return this.configurationModel; } reprocess(): void { @@ -114,7 +133,7 @@ export class FolderSettingsModelParser extends ConfigurationModelParser { } } const configurationModel = this.parseRaw(rawWorkspaceSettings); - this._workspaceSettingsModel = new WorkspaceSettingsModel(configurationModel.contents, configurationModel.keys, configurationModel.overrides, unsupportedKeys); + this._settingsModel = new SettingsModel(configurationModel.contents, configurationModel.keys, configurationModel.overrides, unsupportedKeys); } private getScope(key: string, configurationProperties: { [qualifiedKey: string]: IConfigurationPropertySchema }): ConfigurationScope { diff --git a/src/vs/workbench/services/configuration/node/configuration.ts b/src/vs/workbench/services/configuration/node/configuration.ts index 983bf2b902f..897e2c0faac 100644 --- a/src/vs/workbench/services/configuration/node/configuration.ts +++ b/src/vs/workbench/services/configuration/node/configuration.ts @@ -16,7 +16,7 @@ import { FileChangeType, FileChangesEvent } from 'vs/platform/files/common/files import { isLinux } from 'vs/base/common/platform'; import { ConfigWatcher } from 'vs/base/node/config'; import { ConfigurationModel, ConfigurationModelParser } from 'vs/platform/configuration/common/configurationModels'; -import { WorkspaceConfigurationModelParser, FolderSettingsModelParser, StandaloneConfigurationModelParser, WorkspaceSettingsModel } from 'vs/workbench/services/configuration/common/configurationModels'; +import { WorkspaceConfigurationModelParser, FolderSettingsModelParser, StandaloneConfigurationModelParser } from 'vs/workbench/services/configuration/common/configurationModels'; import { WORKSPACE_STANDALONE_CONFIGURATIONS, FOLDER_SETTINGS_PATH, TASKS_CONFIGURATION_KEY, LAUNCH_CONFIGURATION_KEY } from 'vs/workbench/services/configuration/common/configuration'; import { IStoredWorkspace, IStoredWorkspaceFolder } from 'vs/platform/workspaces/common/workspaces'; import * as extfs from 'vs/base/node/extfs'; @@ -82,6 +82,9 @@ export class WorkspaceConfiguration extends Disposable { private _onDidUpdateConfiguration: Emitter = this._register(new Emitter()); public readonly onDidUpdateConfiguration: Event = this._onDidUpdateConfiguration.event; + private _workspaceConfigurationModelParser: WorkspaceConfigurationModelParser = new WorkspaceConfigurationModelParser(this._workspaceConfigPath ? this._workspaceConfigPath.fsPath : ''); + private _cache: ConfigurationModel = new ConfigurationModel(); + load(workspaceConfigPath: URI): TPromise { if (this._workspaceConfigPath && this._workspaceConfigPath.fsPath === workspaceConfigPath.fsPath) { return this.reload(); @@ -98,20 +101,17 @@ export class WorkspaceConfiguration extends Disposable { onError: error => errors.onUnexpectedError(error), defaultConfig, parse: (content: string, parseErrors: any[]) => { - const workspaceConfigurationModel = new WorkspaceConfigurationModelParser(this._workspaceConfigPath.fsPath); - workspaceConfigurationModel.parse(content); - parseErrors = [...workspaceConfigurationModel.errors]; - return workspaceConfigurationModel; + this._workspaceConfigurationModelParser = new WorkspaceConfigurationModelParser(this._workspaceConfigPath.fsPath); + this._workspaceConfigurationModelParser.parse(content); + parseErrors = [...this._workspaceConfigurationModelParser.errors]; + this.consolidate(); + return this._workspaceConfigurationModelParser; }, initCallback: () => c(null) }); this.listenToWatcher(); }); } - private get workspaceConfigurationModelParser(): WorkspaceConfigurationModelParser { - return this._workspaceConfigurationWatcher ? this._workspaceConfigurationWatcher.getConfig() : new WorkspaceConfigurationModelParser(this._workspaceConfigPath ? this._workspaceConfigPath.fsPath : ''); - } - reload(): TPromise { this.stopListeningToWatcher(); return new TPromise(c => this._workspaceConfigurationWatcher.reload(() => { @@ -121,7 +121,7 @@ export class WorkspaceConfiguration extends Disposable { } getFolders(): IStoredWorkspaceFolder[] { - return this.workspaceConfigurationModelParser.folders; + return this._workspaceConfigurationModelParser.folders; } setFolders(folders: IStoredWorkspaceFolder[], jsonEditingService: JSONEditingService): TPromise { @@ -130,15 +130,16 @@ export class WorkspaceConfiguration extends Disposable { } getConfiguration(): ConfigurationModel { - return this.workspaceConfigurationModelParser.workspaceSettingsModel; + return this._cache; } - getWorkspaceSettings(): WorkspaceSettingsModel { - return this.workspaceConfigurationModelParser.workspaceSettingsModel; + getUnsupportedKeys(): string[] { + return this._workspaceConfigurationModelParser.settingsModel.unsupportedKeys; } reprocessWorkspaceSettings(): ConfigurationModel { - this.workspaceConfigurationModelParser.reprocessWorkspaceSettings(); + this._workspaceConfigurationModelParser.reprocessWorkspaceSettings(); + this.consolidate(); return this.getConfiguration(); } @@ -151,6 +152,10 @@ export class WorkspaceConfiguration extends Disposable { this._workspaceConfigurationWatcherDisposables = dispose(this._workspaceConfigurationWatcherDisposables); } + private consolidate(): void { + this._cache = this._workspaceConfigurationModelParser.settingsModel.merge(this._workspaceConfigurationModelParser.launchModel); + } + dispose(): void { dispose(this._workspaceConfigurationWatcherDisposables); super.dispose(); @@ -190,20 +195,20 @@ export class FolderConfiguration extends Disposable { } reprocess(): ConfigurationModel { - const oldContents = this._folderSettingsModelParser.folderSettingsModel.contents; + const oldContents = this._folderSettingsModelParser.settingsModel.contents; this._folderSettingsModelParser.reprocess(); - if (!equals(oldContents, this._folderSettingsModelParser.folderSettingsModel.contents)) { + if (!equals(oldContents, this._folderSettingsModelParser.settingsModel.contents)) { this.consolidate(); } return this._cache; } getUnsupportedKeys(): string[] { - return this._folderSettingsModelParser.folderSettingsModel.unsupportedKeys; + return this._folderSettingsModelParser.settingsModel.unsupportedKeys; } private consolidate(): void { - this._cache = this._folderSettingsModelParser.folderSettingsModel.merge(...this._standAloneConfigurations); + this._cache = this._folderSettingsModelParser.settingsModel.merge(...this._standAloneConfigurations); } private loadWorkspaceConfigFiles(): TPromise<{ [relativeWorkspacePath: string]: ConfigurationModelParser }> { diff --git a/src/vs/workbench/services/configuration/node/configurationEditingService.ts b/src/vs/workbench/services/configuration/node/configurationEditingService.ts index faad43ebd7d..f91fcb78f79 100644 --- a/src/vs/workbench/services/configuration/node/configurationEditingService.ts +++ b/src/vs/workbench/services/configuration/node/configurationEditingService.ts @@ -390,8 +390,8 @@ export class ConfigurationEditingService { return this.wrapError(ConfigurationEditingErrorCode.ERROR_INVALID_USER_TARGET, target, operation); } - // Workspace tasks and launches are not supported - if (this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE && operation.target === ConfigurationTarget.WORKSPACE) { + // Workspace tasks are not supported + if (operation.workspaceStandAloneConfigurationKey === TASKS_CONFIGURATION_KEY && this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE && operation.target === ConfigurationTarget.WORKSPACE) { return this.wrapError(ConfigurationEditingErrorCode.ERROR_INVALID_WORKSPACE_TARGET, target, operation); } } @@ -432,8 +432,6 @@ export class ConfigurationEditingService { private getConfigurationEditOperation(target: ConfigurationTarget, config: IConfigurationValue, overrides: IConfigurationOverrides): IConfigurationEditOperation { - const workspace = this.contextService.getWorkspace(); - // Check for standalone workspace configurations if (config.key) { const standaloneConfigurationKeys = Object.keys(WORKSPACE_STANDALONE_CONFIGURATIONS); @@ -443,14 +441,14 @@ export class ConfigurationEditingService { // Check for prefix if (config.key === key) { - const jsonPath = workspace.configuration && resource && workspace.configuration.fsPath === resource.fsPath ? [key] : []; + const jsonPath = this.isWorkspaceConfigurationResource(resource) ? [key] : []; return { key: jsonPath[jsonPath.length - 1], jsonPath, value: config.value, resource, workspaceStandAloneConfigurationKey: key, target }; } // Check for prefix. const keyPrefix = `${key}.`; if (config.key.indexOf(keyPrefix) === 0) { - const jsonPath = workspace.configuration && resource && workspace.configuration.fsPath === resource.fsPath ? [key, config.key.substr(keyPrefix.length)] : [config.key.substr(keyPrefix.length)]; + const jsonPath = this.isWorkspaceConfigurationResource(resource) ? [key, config.key.substr(keyPrefix.length)] : [config.key.substr(keyPrefix.length)]; return { key: jsonPath[jsonPath.length - 1], jsonPath, value: config.value, resource, workspaceStandAloneConfigurationKey: key, target }; } } @@ -463,12 +461,17 @@ export class ConfigurationEditingService { } const resource = this.getConfigurationFileResource(target, FOLDER_SETTINGS_PATH, overrides.resource); - if (workspace.configuration && resource && workspace.configuration.fsPath === resource.fsPath) { + if (this.isWorkspaceConfigurationResource(resource)) { jsonPath = ['settings', ...jsonPath]; } return { key, jsonPath, value: config.value, resource, target }; } + private isWorkspaceConfigurationResource(resource: URI): boolean { + const workspace = this.contextService.getWorkspace(); + return workspace.configuration && resource && workspace.configuration.fsPath === resource.fsPath; + } + private getConfigurationFileResource(target: ConfigurationTarget, relativePath: string, resource: URI): URI { if (target === ConfigurationTarget.USER) { return URI.file(this.environmentService.appSettingsPath); diff --git a/src/vs/workbench/services/configuration/node/configurationService.ts b/src/vs/workbench/services/configuration/node/configurationService.ts index 8162473cd97..577c4f78dcd 100644 --- a/src/vs/workbench/services/configuration/node/configurationService.ts +++ b/src/vs/workbench/services/configuration/node/configurationService.ts @@ -288,7 +288,7 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat } getUnsupportedWorkspaceKeys(): string[] { - const unsupportedWorkspaceKeys = [...this.workspaceConfiguration.getWorkspaceSettings().unsupportedKeys]; + const unsupportedWorkspaceKeys = [...this.workspaceConfiguration.getUnsupportedKeys()]; for (const folder of this.workspace.folders) { unsupportedWorkspaceKeys.push(...this.cachedFolderConfigs.get(folder.uri).getUnsupportedKeys()); } diff --git a/src/vs/workbench/services/configuration/test/node/configurationService.test.ts b/src/vs/workbench/services/configuration/test/node/configurationService.test.ts index 37565648b9f..6563fd4a42c 100644 --- a/src/vs/workbench/services/configuration/test/node/configurationService.test.ts +++ b/src/vs/workbench/services/configuration/test/node/configurationService.test.ts @@ -1066,6 +1066,56 @@ suite('WorkspaceConfigurationService - Multiroot', () => { }); }); + test('get launch configuration', () => { + const expectedLaunchConfiguration = { + 'version': '0.1.0', + 'configurations': [ + { + 'type': 'node', + 'request': 'launch', + 'name': 'Gulp Build', + 'program': '${workspaceFolder}/node_modules/gulp/bin/gulp.js', + 'stopOnEntry': true, + 'args': [ + 'watch-extension:json-client' + ], + 'cwd': '${workspaceFolder}' + } + ] + }; + return jsonEditingServce.write(workspaceContextService.getWorkspace().configuration, { key: 'launch', value: expectedLaunchConfiguration }, true) + .then(() => testObject.reloadConfiguration()) + .then(() => { + const actual = testObject.getValue('launch'); + assert.deepEqual(actual, expectedLaunchConfiguration); + }); + }); + + test('inspect launch configuration', () => { + const expectedLaunchConfiguration = { + 'version': '0.1.0', + 'configurations': [ + { + 'type': 'node', + 'request': 'launch', + 'name': 'Gulp Build', + 'program': '${workspaceFolder}/node_modules/gulp/bin/gulp.js', + 'stopOnEntry': true, + 'args': [ + 'watch-extension:json-client' + ], + 'cwd': '${workspaceFolder}' + } + ] + }; + return jsonEditingServce.write(workspaceContextService.getWorkspace().configuration, { key: 'launch', value: expectedLaunchConfiguration }, true) + .then(() => testObject.reloadConfiguration()) + .then(() => { + const actual = testObject.inspect('launch').workspace; + assert.deepEqual(actual, expectedLaunchConfiguration); + }); + }); + test('update user configuration', () => { return testObject.updateValue('configurationService.workspace.testSetting', 'userValue', ConfigurationTarget.USER) .then(() => assert.equal(testObject.getValue('configurationService.workspace.testSetting'), 'userValue')); @@ -1116,10 +1166,10 @@ suite('WorkspaceConfigurationService - Multiroot', () => { .then(() => assert.fail('Should not be supported'), (e) => assert.equal(e.code, ConfigurationEditingErrorCode.ERROR_INVALID_WORKSPACE_TARGET)); }); - test('update launch configuration in a workspace is not supported', () => { + test('update launch configuration in a workspace', () => { const workspace = workspaceContextService.getWorkspace(); return testObject.updateValue('launch', { 'version': '1.0.0', configurations: [{ 'name': 'myLaunch' }] }, { resource: workspace.folders[0].uri }, ConfigurationTarget.WORKSPACE, true) - .then(() => assert.fail('Should not be supported'), (e) => assert.equal(e.code, ConfigurationEditingErrorCode.ERROR_INVALID_WORKSPACE_TARGET)); + .then(() => assert.deepEqual(testObject.getValue('launch'), { 'version': '1.0.0', configurations: [{ 'name': 'myLaunch' }] })); }); test('task configurations are not read from workspace', () => { @@ -1130,13 +1180,4 @@ suite('WorkspaceConfigurationService - Multiroot', () => { assert.equal(actual.workspace, void 0); }); }); - - test('launch configurations are not read from workspace', () => { - return jsonEditingServce.write(workspaceContextService.getWorkspace().configuration, { key: 'launch', value: { 'version': '1.0' } }, true) - .then(() => testObject.reloadConfiguration()) - .then(() => { - const actual = testObject.inspect('launch.version'); - assert.equal(actual.workspace, void 0); - }); - }); });