From e410cde37dc436816022d88ec8ca40c0273e9def Mon Sep 17 00:00:00 2001 From: DavidPortoUP Date: Wed, 22 Nov 2017 13:26:52 +0000 Subject: [PATCH 001/128] Added selectToBracket() --- .../bracketMatching/bracketMatching.ts | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/src/vs/editor/contrib/bracketMatching/bracketMatching.ts b/src/vs/editor/contrib/bracketMatching/bracketMatching.ts index fbd1a37d39c..13026a7cf2d 100644 --- a/src/vs/editor/contrib/bracketMatching/bracketMatching.ts +++ b/src/vs/editor/contrib/bracketMatching/bracketMatching.ts @@ -148,6 +148,55 @@ export class BracketMatchingController extends Disposable implements editorCommo this._editor.revealRange(newSelections[0]); } + public selectToBracket(): void{ + //getting the model //trying to find + const model = this._editor.getModel(); + if (!model) { + return; + } + + //get the current position of the editor, so it can be + //used in the next step + let openBracket: Position; + let closeBracket: Position; + + let newSelections = this._editor.getSelections().map(selection => { + const position = selection.getStartPosition(); + const brackets = model.matchBracket(position); + let newCursorPosition: Position = null; + if (brackets) { + openBracket = brackets[0]; + closeBracket = brackets[1]; + if (openBracket.containsPosition(position)) { + newCursorPosition = closeBracket.getStartPosition(); + } else if (closeBracket.containsPosition(position)) { + newCursorPosition = openBracket.getStartPosition(); + } else { + // find the next bracket if the position isn't on a matching bracket + const nextBracket = model.findNextBracket(position); + if (nextBracket && nextBracket.range) { + newCursorPosition = nextBracket.range.getStartPosition(); + } + } + } + } + if (openBracket && closeBracket) { + this._selectContentWithinBrackets(openBracket, closeBracket); + } + } + +//By the first pull request guy +//selecting text between the open and close bracket +private _selectContentWithinBrackets(openBracket: Position, closeBracket: Position): void { + const bracketRange: Range = new Range( + openBracket.lineNumber, + openBracket.column, + closeBracket.lineNumber, + closeBracket.column + ); + this._editor.setSelection(bracketRange); +} + private static readonly _DECORATION_OPTIONS = ModelDecorationOptions.register({ stickiness: editorCommon.TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, className: 'bracket-match' From 8727487b158601f537bc1f21784eb56fc9843485 Mon Sep 17 00:00:00 2001 From: Afonso Pinto Date: Thu, 23 Nov 2017 22:16:23 +0000 Subject: [PATCH 002/128] basic select to bracket; unit tests --- .../bracketMatching/bracketMatching.ts | 102 +++++++++++------- .../test/bracketMatching.test.ts | 46 ++++++++ 2 files changed, 109 insertions(+), 39 deletions(-) diff --git a/src/vs/editor/contrib/bracketMatching/bracketMatching.ts b/src/vs/editor/contrib/bracketMatching/bracketMatching.ts index 13026a7cf2d..4cb6957804c 100644 --- a/src/vs/editor/contrib/bracketMatching/bracketMatching.ts +++ b/src/vs/editor/contrib/bracketMatching/bracketMatching.ts @@ -14,14 +14,19 @@ 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'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModelWithDecorations'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -class SelectBracketAction extends EditorAction { +class JumpToBracketAction extends EditorAction { constructor() { super({ id: 'editor.action.jumpToBracket', @@ -44,6 +49,29 @@ 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, + kbOpts: { + kbExpr: EditorContextKeys.textFocus, + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_Q + } + }); + } + + public run(accessor: ServicesAccessor, editor: ICodeEditor): void { + let controller = BracketMatchingController.get(editor); + if (!controller) { + return; + } + controller.selectToBracket(); + } +} + type Brackets = [Range, Range]; class BracketsData { @@ -148,54 +176,49 @@ export class BracketMatchingController extends Disposable implements editorCommo this._editor.revealRange(newSelections[0]); } - public selectToBracket(): void{ - //getting the model //trying to find + public selectToBracket(): void { const model = this._editor.getModel(); if (!model) { return; } + const selection = this._editor.getSelection(); + if (!selection.isEmpty()) { + return; + } - //get the current position of the editor, so it can be - //used in the next step - let openBracket: Position; - let closeBracket: Position; + const position = selection.getStartPosition(); - let newSelections = this._editor.getSelections().map(selection => { - const position = selection.getStartPosition(); - const brackets = model.matchBracket(position); - let newCursorPosition: Position = null; - if (brackets) { - openBracket = brackets[0]; - closeBracket = brackets[1]; - if (openBracket.containsPosition(position)) { - newCursorPosition = closeBracket.getStartPosition(); - } else if (closeBracket.containsPosition(position)) { - newCursorPosition = openBracket.getStartPosition(); - } else { - // find the next bracket if the position isn't on a matching bracket - const nextBracket = model.findNextBracket(position); - if (nextBracket && nextBracket.range) { - newCursorPosition = nextBracket.range.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._selectContentWithinBrackets(openBracket, closeBracket); + this._editor.setSelection(new Range(openBracket.lineNumber, openBracket.column, closeBracket.lineNumber, closeBracket.column)); } } -//By the first pull request guy -//selecting text between the open and close bracket -private _selectContentWithinBrackets(openBracket: Position, closeBracket: Position): void { - const bracketRange: Range = new Range( - openBracket.lineNumber, - openBracket.column, - closeBracket.lineNumber, - closeBracket.column - ); - this._editor.setSelection(bracketRange); -} private static readonly _DECORATION_OPTIONS = ModelDecorationOptions.register({ stickiness: editorCommon.TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, @@ -276,7 +299,8 @@ private _selectContentWithinBrackets(openBracket: Position, closeBracket: Positi } 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 7e7d5b977ec..57e72f34fd5 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 { Model } from 'vs/editor/common/model/model'; 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 = Model.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(); + }); }); From 72e3cf950a0f7ba83e9160d0f066123097119aa8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Oliveira?= Date: Thu, 23 Nov 2017 22:57:03 +0000 Subject: [PATCH 003/128] Formatting --- src/vs/editor/contrib/bracketMatching/bracketMatching.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/vs/editor/contrib/bracketMatching/bracketMatching.ts b/src/vs/editor/contrib/bracketMatching/bracketMatching.ts index 4cb6957804c..b6d5c1ace8d 100644 --- a/src/vs/editor/contrib/bracketMatching/bracketMatching.ts +++ b/src/vs/editor/contrib/bracketMatching/bracketMatching.ts @@ -14,12 +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 { - EditorAction, - registerEditorAction, - registerEditorContribution, - ServicesAccessor -} 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'; From 86f0a340fe22cfc18bcfb1ffd4f065c83bd315cc Mon Sep 17 00:00:00 2001 From: Nelson Almeida Date: Wed, 29 Nov 2017 13:38:08 +0000 Subject: [PATCH 004/128] Update fontInfo.ts --- src/vs/editor/common/config/fontInfo.ts | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) 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; From 522052931f780f5f948f7da6a7c204eced3164a1 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Sat, 13 Jan 2018 18:23:17 +0100 Subject: [PATCH 005/128] clean up icon path confusion --- src/vs/code/electron-main/window.ts | 2 +- .../actions/browser/menuItemActionItem.ts | 52 ++++++++++++++++--- src/vs/platform/actions/common/actions.ts | 5 +- .../electron-browser/menusExtensionPoint.ts | 31 ++++------- .../electron-browser/media/codeEditor.css | 8 --- .../electron-browser/toggleWordWrap.ts | 5 +- .../fileActions.contribution.ts | 16 ++++-- .../electron-browser/media/fileactions.css | 16 ------ 8 files changed, 72 insertions(+), 63 deletions(-) delete mode 100644 src/vs/workbench/parts/codeEditor/electron-browser/media/codeEditor.css diff --git a/src/vs/code/electron-main/window.ts b/src/vs/code/electron-main/window.ts index 4c37dd6e993..bbe6e486f12 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(typeof item.iconPath === 'string' ? item.iconPath : item.iconPath.dark); if (icon.isEmpty()) { icon = void 0; } diff --git a/src/vs/platform/actions/browser/menuItemActionItem.ts b/src/vs/platform/actions/browser/menuItemActionItem.ts index 5d81043100e..787ae98fb14 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, + private 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 (typeof item.iconPath === 'string') { + if (MenuItemActionItem.ICON_PATH_TO_CSS_RULES.has(item.iconPath)) { + iconClass = MenuItemActionItem.ICON_PATH_TO_CSS_RULES.get(item.iconPath); + } else { + iconClass = ids.nextId(); + createCSSRule(`.icon.${iconClass}`, `background-image: url("${URI.file(item.iconPath).toString()}")`); + MenuItemActionItem.ICON_PATH_TO_CSS_RULES.set(item.iconPath, iconClass); + } + } else { + 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).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) }; } } } diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index 5e0eaceed36..40703ce8563 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?: string | { light: string; dark: 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..be1f3f20705 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: string | { light: string; dark: string; }; + if (icon) { + if (typeof icon === 'string') { + absoluteIcon = 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/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..2cbe5f7900f 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: 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: 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/files/electron-browser/fileActions.contribution.ts b/src/vs/workbench/parts/files/electron-browser/fileActions.contribution.ts index f9e193a5a78..12350af78a8 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; } From a89ed2b1ee82fe72e1370e2f8d709ba2c1bafcff Mon Sep 17 00:00:00 2001 From: chrisdias Date: Mon, 15 Jan 2018 17:39:50 -0800 Subject: [PATCH 006/128] fixes #26044 --- .../platform/contextkey/common/contextkey.ts | 64 ++++++++++++++++++- .../contextkey/test/common/contextkey.test.ts | 11 +++- 2 files changed, 73 insertions(+), 2 deletions(-) diff --git a/src/vs/platform/contextkey/common/contextkey.ts b/src/vs/platform/contextkey/common/contextkey.ts index 69381904e0a..f578e60070a 100644 --- a/src/vs/platform/contextkey/common/contextkey.ts +++ b/src/vs/platform/contextkey/common/contextkey.ts @@ -6,13 +6,15 @@ 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, Not = 2, Equals = 3, NotEquals = 4, - And = 5 + And = 5, + Glob = 6 } export abstract class ContextKeyExpr { @@ -29,6 +31,10 @@ export abstract class ContextKeyExpr { return new ContextKeyNotEqualsExpr(key, value); } + public static glob(key: string, value: string): ContextKeyExpr { + return new ContextKeyGlobExpr(key, value); + } + public static not(key: string): ContextKeyExpr { return new ContextKeyNotExpr(key); } @@ -60,6 +66,11 @@ export abstract class ContextKeyExpr { return new ContextKeyEqualsExpr(pieces[0].trim(), this._deserializeValue(pieces[1])); } + if (serializedOne.indexOf('=~') >= 0) { + let pieces = serializedOne.split('=~'); + return new ContextKeyGlobExpr(pieces[0].trim(), this._deserializeValue(pieces[1])); + } + if (/^\!\s*/.test(serializedOne)) { return new ContextKeyNotExpr(serializedOne.substr(1).trim()); } @@ -109,6 +120,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); default: throw new Error('Unknown ContextKeyExpr!'); } @@ -320,6 +333,55 @@ export class ContextKeyNotExpr implements ContextKeyExpr { } } +export class ContextKeyGlobExpr implements ContextKeyExpr { + + constructor(private key: string, private value: any) { + } + + public getType(): ContextKeyExprType { + return ContextKeyExprType.Glob; + } + + public cmp(other: ContextKeyGlobExpr): number { + if (this.key < other.key) { + return -1; + } + if (this.key > other.key) { + return 1; + } + if (this.value < other.value) { + return -1; + } + if (this.value > other.value) { + return 1; + } + return 0; + } + + public equals(other: ContextKeyExpr): boolean { + if (other instanceof ContextKeyGlobExpr) { + return (this.key === other.key && this.value === other.value); + } + return false; + } + + public evaluate(context: IContext): boolean { + return match(this.value, context.getValue(this.key)) + } + + public normalize(): ContextKeyExpr { + return this; + } + + public serialize(): string { + return this.key + ' =~ \'' + this.value + '\''; + } + + public keys(): string[] { + return [this.key]; + } +} + export class ContextKeyAndExpr implements ContextKeyExpr { public readonly expr: ContextKeyExpr[]; diff --git a/src/vs/platform/contextkey/test/common/contextkey.test.ts b/src/vs/platform/contextkey/test/common/contextkey.test.ts index 69f8bca0930..4cdda5588c5 100644 --- a/src/vs/platform/contextkey/test/common/contextkey.test.ts +++ b/src/vs/platform/contextkey/test/common/contextkey.test.ts @@ -6,6 +6,7 @@ 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 { @@ -21,6 +22,8 @@ suite('ContextKeyExpr', () => { ContextKeyExpr.has('a1'), ContextKeyExpr.and(ContextKeyExpr.has('and.a')), ContextKeyExpr.has('a2'), + ContextKeyExpr.glob('d3', '**/d*'), + ContextKeyExpr.glob('d4', '**/3*'), ContextKeyExpr.equals('b1', 'bb1'), ContextKeyExpr.equals('b2', 'bb2'), ContextKeyExpr.notEquals('c1', 'cc1'), @@ -32,9 +35,11 @@ suite('ContextKeyExpr', () => { ContextKeyExpr.equals('b2', 'bb2'), ContextKeyExpr.notEquals('c1', 'cc1'), ContextKeyExpr.not('d1'), + ContextKeyExpr.glob('d4', '**/3*'), ContextKeyExpr.notEquals('c2', 'cc2'), ContextKeyExpr.has('a2'), ContextKeyExpr.equals('b1', 'bb1'), + ContextKeyExpr.glob('d3', '**/d*'), ContextKeyExpr.has('a1'), ContextKeyExpr.and(ContextKeyExpr.equals('and.a', true)), ContextKeyExpr.not('d2') @@ -59,9 +64,11 @@ suite('ContextKeyExpr', () => { let context = createContext({ 'a': true, 'b': false, - 'c': '5' + 'c': '5', + 'd': 'd' }); function testExpression(expr: string, expected: boolean): void { + console.log(expr + ' ' + expected); let rules = ContextKeyExpr.deserialize(expr); assert.equal(rules.evaluate(context), expected, expr); } @@ -74,11 +81,13 @@ suite('ContextKeyExpr', () => { testExpression(expr + ' == 5', value == '5'); testExpression(expr + ' != 5', value != '5'); testExpression('!' + expr, !value); + testExpression(expr + ' =~ **/d*', match('**/d*', value)); } testBatch('a', true); testBatch('b', false); testBatch('c', '5'); + testBatch('d', 'd'); testBatch('z', undefined); testExpression('a && !b', true && !false); From df96558335782a39ef4b2df9bdbee0612ec63eed Mon Sep 17 00:00:00 2001 From: chrisdias Date: Mon, 15 Jan 2018 17:40:10 -0800 Subject: [PATCH 007/128] hygene --- src/vs/platform/contextkey/common/contextkey.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/platform/contextkey/common/contextkey.ts b/src/vs/platform/contextkey/common/contextkey.ts index f578e60070a..5461a40cf07 100644 --- a/src/vs/platform/contextkey/common/contextkey.ts +++ b/src/vs/platform/contextkey/common/contextkey.ts @@ -366,7 +366,7 @@ export class ContextKeyGlobExpr implements ContextKeyExpr { } public evaluate(context: IContext): boolean { - return match(this.value, context.getValue(this.key)) + return match(this.value, context.getValue(this.key)); } public normalize(): ContextKeyExpr { From 460e00c5756596858d2a2038a4cc225b836562a4 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Thu, 18 Jan 2018 08:59:43 +0100 Subject: [PATCH 008/128] update inno --- build/win32/code.iss | 64 +++++++++++++++++++++++++++++++---- build/win32/inno_updater.exe | Bin 0 -> 178176 bytes 2 files changed, 57 insertions(+), 7 deletions(-) create mode 100644 build/win32/inno_updater.exe diff --git a/build/win32/code.iss b/build/win32/code.iss index f4374ee099d..ed747315c63 100644 --- a/build/win32/code.iss +++ b/build/win32/code.iss @@ -18,7 +18,7 @@ OutputDir={#OutputDir} OutputBaseFilename=VSCodeSetup Compression=lzma SolidCompression=yes -AppMutex={#AppMutex} +AppMutex={code:GetAppMutex} SetupMutex={#AppMutex}setup WizardImageFile={#RepoDir}\resources\win32\inno-big.bmp WizardSmallImageFile={#RepoDir}\resources\win32\inno-small.bmp @@ -47,11 +47,15 @@ Name: "simplifiedChinese"; MessagesFile: "{#RepoDir}\build\win32\i18n\Default.zh Name: "traditionalChinese"; MessagesFile: "{#RepoDir}\build\win32\i18n\Default.zh-tw.isl,{#RepoDir}\build\win32\i18n\messages.zh-tw.isl" {#LocalizedLanguageFile("cht")} [InstallDelete] -Type: filesandordirs; Name: {app}\resources\app\out -Type: filesandordirs; Name: {app}\resources\app\plugins -Type: filesandordirs; Name: {app}\resources\app\extensions -Type: filesandordirs; Name: {app}\resources\app\node_modules -Type: files; Name: {app}\resources\app\Credits_45.0.2454.85.html +Type: filesandordirs; Name: "{app}\resources\app\out" +Type: filesandordirs; Name: "{app}\resources\app\plugins" +Type: filesandordirs; Name: "{app}\resources\app\extensions" +Type: filesandordirs; Name: "{app}\resources\app\node_modules" +Type: files; Name: "{app}\resources\app\Credits_45.0.2454.85.html" + +[UninstallDelete] +Type: filesandordirs; Name: "{app}\_" +Type: filesandordirs; Name: "{app}\old" [Tasks] Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked @@ -63,7 +67,8 @@ Name: "addtopath"; Description: "{cm:AddToPath}"; GroupDescription: "{cm:Other}" Name: "runcode"; Description: "{cm:RunAfter,{#NameShort}}"; GroupDescription: "{cm:Other}"; Check: WizardSilent [Files] -Source: "*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs +Source: "*"; DestDir: "{code:GetDestDir}"; Flags: ignoreversion recursesubdirs createallsubdirs +Source: "{#RepoDir}\build\win32\inno_updater.exe"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs [Icons] Name: "{group}\{#NameLong}"; Filename: "{app}\{#ExeBasename}.exe"; AppUserModelID: "{#AppUserId}" @@ -955,6 +960,51 @@ begin Result := not WizardSilent(); end; +// Updates +function IsUpdate(): Boolean; +begin + Result := ExpandConstant('{param:update|false}') = 'true'; +end; + +function GetAppMutex(Value: string): string; +begin + if IsUpdate() then + Result := '' + else + Result := '{#AppMutex}'; +end; + +function GetDestDir(Value: string): string; +begin + if IsUpdate() then + Result := ExpandConstant('{app}\_') + else + Result := ExpandConstant('{app}'); +end; + +procedure CurStepChanged(CurStep: TSetupStep); +var + UpdateResultCode: Integer; +begin + if (CurStep = ssPostInstall) and (ExpandConstant('{param:update|false}') = 'true') then + begin + CreateMutex('{#AppMutex}-ready'); + + while (CheckForMutexes('{#AppMutex}')) do + begin + Log('Application is still running, waiting'); + Sleep(1000); + end; + + Sleep(1000); + + if Exec(ExpandConstant('{app}\inno_updater.exe'), ExpandConstant('--apply-update _ "{app}\unins000.dat"'), '', SW_SHOW, ewWaitUntilTerminated, UpdateResultCode) then + Log('Update applied successfully!') + else + Log('Failed to apply update!'); + end; +end; + // http://stackoverflow.com/a/23838239/261019 procedure Explode(var Dest: TArrayOfString; Text: String; Separator: String); var diff --git a/build/win32/inno_updater.exe b/build/win32/inno_updater.exe new file mode 100644 index 0000000000000000000000000000000000000000..3dd7e34c20ba4a75f6dd9b6f593ec183293658fc GIT binary patch literal 178176 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 Date: Thu, 18 Jan 2018 14:56:13 +0100 Subject: [PATCH 009/128] update inno updater, selectively launch code after update --- build/win32/code.iss | 39 ++++++++++++++++++++++------------- build/win32/inno_updater.exe | Bin 178176 -> 180224 bytes 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/build/win32/code.iss b/build/win32/code.iss index ed747315c63..7e8326e4313 100644 --- a/build/win32/code.iss +++ b/build/win32/code.iss @@ -47,11 +47,11 @@ Name: "simplifiedChinese"; MessagesFile: "{#RepoDir}\build\win32\i18n\Default.zh Name: "traditionalChinese"; MessagesFile: "{#RepoDir}\build\win32\i18n\Default.zh-tw.isl,{#RepoDir}\build\win32\i18n\messages.zh-tw.isl" {#LocalizedLanguageFile("cht")} [InstallDelete] -Type: filesandordirs; Name: "{app}\resources\app\out" -Type: filesandordirs; Name: "{app}\resources\app\plugins" -Type: filesandordirs; Name: "{app}\resources\app\extensions" -Type: filesandordirs; Name: "{app}\resources\app\node_modules" -Type: files; Name: "{app}\resources\app\Credits_45.0.2454.85.html" +Type: filesandordirs; Name: "{app}\resources\app\out"; Check: IsNotUpdate +Type: filesandordirs; Name: "{app}\resources\app\plugins"; Check: IsNotUpdate +Type: filesandordirs; Name: "{app}\resources\app\extensions"; Check: IsNotUpdate +Type: filesandordirs; Name: "{app}\resources\app\node_modules"; Check: IsNotUpdate +Type: files; Name: "{app}\resources\app\Credits_45.0.2454.85.html"; Check: IsNotUpdate [UninstallDelete] Type: filesandordirs; Name: "{app}\_" @@ -76,7 +76,7 @@ Name: "{commondesktop}\{#NameLong}"; Filename: "{app}\{#ExeBasename}.exe"; Tasks Name: "{userappdata}\Microsoft\Internet Explorer\Quick Launch\{#NameLong}"; Filename: "{app}\{#ExeBasename}.exe"; Tasks: quicklaunchicon; AppUserModelID: "{#AppUserId}" [Run] -Filename: "{app}\{#ExeBasename}.exe"; Description: "{cm:LaunchProgram,{#NameLong}}"; Tasks: runcode; Flags: nowait postinstall; Check: WizardSilent +Filename: "{app}\{#ExeBasename}.exe"; Description: "{cm:LaunchProgram,{#NameLong}}"; Tasks: runcode; Flags: nowait postinstall; Check: ShouldRunAfterUpdate Filename: "{app}\{#ExeBasename}.exe"; Description: "{cm:LaunchProgram,{#NameLong}}"; Flags: nowait postinstall; Check: WizardNotSilent [Registry] @@ -963,7 +963,23 @@ end; // Updates function IsUpdate(): Boolean; begin - Result := ExpandConstant('{param:update|false}') = 'true'; + Result := ExpandConstant('{param:update|false}') <> 'false'; +end; + +function IsNotUpdate(): Boolean; +begin + Result := not IsUpdate(); +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 ShouldRunAfterUpdate(): Boolean; +begin + if IsUpdate() then + Result := not FileExists(ExpandConstant('{param:update}')) + else + Result := False; end; function GetAppMutex(Value: string): string; @@ -986,7 +1002,7 @@ procedure CurStepChanged(CurStep: TSetupStep); var UpdateResultCode: Integer; begin - if (CurStep = ssPostInstall) and (ExpandConstant('{param:update|false}') = 'true') then + if IsUpdate() and (CurStep = ssPostInstall) then begin CreateMutex('{#AppMutex}-ready'); @@ -996,12 +1012,7 @@ begin Sleep(1000); end; - Sleep(1000); - - if Exec(ExpandConstant('{app}\inno_updater.exe'), ExpandConstant('--apply-update _ "{app}\unins000.dat"'), '', SW_SHOW, ewWaitUntilTerminated, UpdateResultCode) then - Log('Update applied successfully!') - else - Log('Failed to apply update!'); + Exec(ExpandConstant('{app}\inno_updater.exe'), ExpandConstant('--apply-update _ "{app}\unins000.dat"'), '', SW_SHOW, ewWaitUntilTerminated, UpdateResultCode); end; end; diff --git a/build/win32/inno_updater.exe b/build/win32/inno_updater.exe index 3dd7e34c20ba4a75f6dd9b6f593ec183293658fc..d82b1430c6407c1bf6a21418629564ddd599956e 100644 GIT binary patch delta 50555 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

_^WXMXCW-uUssGnLfudb3yA@%)fJ%&sWmCHY_U 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 delta 48210 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�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 NZ?55h^5Y1S61h(V-22$#)hVb^rOrU#}sbu z8{JLrW_OD_p*+1jvplAc6X`U?e>&6mNz-d8><7=O?9nxk@fBdZ-c)f@@N7Iupf0Eb&1R_V(Wn%cL_3; zxjpX2@<4g8ytO>CBB3I^BD12jGO{Y6D!s~DWvVsTdTX0%1GUYy!P*d-YC4jB#DbB` z!8itv1Tl{Ox Date: Thu, 18 Jan 2018 16:18:25 +0100 Subject: [PATCH 010/128] publish windows builds as supporting fast updates --- build/tfs/common/publish.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/build/tfs/common/publish.ts b/build/tfs/common/publish.ts index e4cbdc80f0e..8e25b630c3e 100644 --- a/build/tfs/common/publish.ts +++ b/build/tfs/common/publish.ts @@ -69,6 +69,7 @@ interface Asset { hash: string; sha256hash: string; size: number; + supportsFastUpdate?: boolean; } function createOrUpdate(commit: string, quality: string, platform: string, type: string, release: NewDocument, asset: Asset, isUpdate: boolean): Promise { @@ -234,6 +235,11 @@ async function publish(commit: string, quality: string, platform: string, type: size }; + // Remove this if we ever need to rollback fast updates for windows + if (/win32/.test(platform)) { + asset.supportsFastUpdate = true; + } + const release = { id: commit, timestamp: (new Date()).getTime(), From 439ca2e841847eeafa33023541c7524b92dad2bd Mon Sep 17 00:00:00 2001 From: isidor Date: Thu, 18 Jan 2018 16:56:08 +0100 Subject: [PATCH 011/128] debug: launch schema in workspace settings --- .../debug/electron-browser/debugConfigurationManager.ts | 9 ++++----- .../services/configuration/common/configuration.ts | 3 ++- .../configuration/common/configurationExtensionPoint.ts | 8 +++++++- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/parts/debug/electron-browser/debugConfigurationManager.ts b/src/vs/workbench/parts/debug/electron-browser/debugConfigurationManager.ts index 3a779ea68b0..14329c56bf2 100644 --- a/src/vs/workbench/parts/debug/electron-browser/debugConfigurationManager.ts +++ b/src/vs/workbench/parts/debug/electron-browser/debugConfigurationManager.ts @@ -33,6 +33,7 @@ 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'; // debuggers extension point export const debuggersExtPoint = extensionsRegistry.ExtensionsRegistry.registerExtensionPoint('debuggers', [], { @@ -147,14 +148,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 +200,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'; 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 bdd9c346c22..7386395fbc5 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: {}, From 178f9a95b61ade9dbc40e42cf7077fea7514e397 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Fri, 19 Jan 2018 11:31:36 +0100 Subject: [PATCH 012/128] wip: windows fast updates --- src/typings/windows-mutex.ts | 2 + src/vs/code/electron-main/menus.ts | 11 ++ src/vs/platform/update/common/update.ts | 10 +- src/vs/platform/update/common/updateIpc.ts | 16 +++ .../electron-main/auto-updater.win32.ts | 74 ++++++++++- .../update/electron-main/updateService.ts | 62 ++++++++-- .../parts/update/electron-browser/update.ts | 116 ++++++++++++------ 7 files changed, 239 insertions(+), 52 deletions(-) diff --git a/src/typings/windows-mutex.ts b/src/typings/windows-mutex.ts index 039dffcc2ad..a3acc8f4430 100644 --- a/src/typings/windows-mutex.ts +++ b/src/typings/windows-mutex.ts @@ -9,4 +9,6 @@ declare module 'windows-mutex' { isActive(): boolean; release(): void; } + + export function isActive(name: string): boolean; } \ No newline at end of file diff --git a/src/vs/code/electron-main/menus.ts b/src/vs/code/electron-main/menus.ts index 59229fbc739..ced1253651e 100644 --- a/src/vs/code/electron-main/menus.ts +++ b/src/vs/code/electron-main/menus.ts @@ -1045,6 +1045,17 @@ export class CodeMenu { return []; case UpdateState.UpdateDownloaded: + return [new MenuItem({ + label: nls.localize('miInstallUpdate', "Install Update..."), click: () => { + this.reportMenuActionTelemetry('InstallUpdate'); + this.updateService.applyUpdate(); + } + })]; + + case UpdateState.UpdateInstalling: + return [new MenuItem({ label: nls.localize('miInstallingUpdate', "Installing Update..."), enabled: false })]; + + case UpdateState.UpdateReady: return [new MenuItem({ label: nls.localize('miRestartToUpdate', "Restart to Update..."), click: () => { this.reportMenuActionTelemetry('RestartToUpdate'); diff --git a/src/vs/platform/update/common/update.ts b/src/vs/platform/update/common/update.ts index 13928e33f17..eecba458299 100644 --- a/src/vs/platform/update/common/update.ts +++ b/src/vs/platform/update/common/update.ts @@ -14,7 +14,9 @@ export enum State { Idle, CheckingForUpdate, UpdateAvailable, - UpdateDownloaded + UpdateDownloaded, + UpdateInstalling, + UpdateReady } export enum ExplicitState { @@ -26,6 +28,7 @@ export interface IRawUpdate { releaseNotes: string; version: string; date: Date; + supportsFastUpdate?: boolean; } export interface IUpdate { @@ -33,11 +36,13 @@ export interface IUpdate { date?: Date; releaseNotes?: string; url?: string; + supportsFastUpdate?: boolean; } export interface IAutoUpdater extends NodeEventEmitter { setFeedURL(url: string): void; checkForUpdates(): void; + applyUpdate?(): TPromise; quitAndInstall(): void; } @@ -49,10 +54,13 @@ export interface IUpdateService { readonly onError: Event; readonly onUpdateAvailable: Event<{ url: string; version: string; }>; readonly onUpdateNotAvailable: Event; + readonly onUpdateDownloaded: Event; + readonly onUpdateInstalling: Event; readonly onUpdateReady: Event; readonly onStateChange: Event; readonly state: State; checkForUpdates(explicit: boolean): TPromise; + applyUpdate(): TPromise; quitAndInstall(): TPromise; } \ No newline at end of file diff --git a/src/vs/platform/update/common/updateIpc.ts b/src/vs/platform/update/common/updateIpc.ts index bd42c38e018..9b8b30041a3 100644 --- a/src/vs/platform/update/common/updateIpc.ts +++ b/src/vs/platform/update/common/updateIpc.ts @@ -15,9 +15,12 @@ export interface IUpdateChannel extends IChannel { call(command: 'event:onError'): TPromise; call(command: 'event:onUpdateAvailable'): TPromise; call(command: 'event:onUpdateNotAvailable'): TPromise; + call(command: 'event:onUpdateDownloaded'): TPromise; + call(command: 'event:onUpdateInstalling'): TPromise; call(command: 'event:onUpdateReady'): TPromise; call(command: 'event:onStateChange'): TPromise; call(command: 'checkForUpdates', arg: boolean): TPromise; + call(command: 'applyUpdate'): TPromise; call(command: 'quitAndInstall'): TPromise; call(command: '_getInitialState'): TPromise; call(command: string, arg?: any): TPromise; @@ -32,9 +35,12 @@ export class UpdateChannel implements IUpdateChannel { case 'event:onError': return eventToCall(this.service.onError); case 'event:onUpdateAvailable': return eventToCall(this.service.onUpdateAvailable); case 'event:onUpdateNotAvailable': return eventToCall(this.service.onUpdateNotAvailable); + case 'event:onUpdateDownloaded': return eventToCall(this.service.onUpdateDownloaded); + case 'event:onUpdateInstalling': return eventToCall(this.service.onUpdateInstalling); case 'event:onUpdateReady': return eventToCall(this.service.onUpdateReady); case 'event:onStateChange': return eventToCall(this.service.onStateChange); case 'checkForUpdates': return this.service.checkForUpdates(arg); + case 'applyUpdate': return this.service.applyUpdate(); case 'quitAndInstall': return this.service.quitAndInstall(); case '_getInitialState': return TPromise.as(this.service.state); } @@ -55,6 +61,12 @@ export class UpdateChannelClient implements IUpdateService { private _onUpdateNotAvailable = eventFromCall(this.channel, 'event:onUpdateNotAvailable'); get onUpdateNotAvailable(): Event { return this._onUpdateNotAvailable; } + private _onUpdateDownloaded = eventFromCall(this.channel, 'event:onUpdateDownloaded'); + get onUpdateDownloaded(): Event { return this._onUpdateDownloaded; } + + private _onUpdateInstalling = eventFromCall(this.channel, 'event:onUpdateInstalling'); + get onUpdateInstalling(): Event { return this._onUpdateInstalling; } + private _onUpdateReady = eventFromCall(this.channel, 'event:onUpdateReady'); get onUpdateReady(): Event { return this._onUpdateReady; } @@ -82,6 +94,10 @@ export class UpdateChannelClient implements IUpdateService { return this.channel.call('checkForUpdates', explicit); } + applyUpdate(): TPromise { + return this.channel.call('applyUpdate'); + } + quitAndInstall(): TPromise { return this.channel.call('quitAndInstall'); } diff --git a/src/vs/platform/update/electron-main/auto-updater.win32.ts b/src/vs/platform/update/electron-main/auto-updater.win32.ts index c88880617f3..401dc510256 100644 --- a/src/vs/platform/update/electron-main/auto-updater.win32.ts +++ b/src/vs/platform/update/electron-main/auto-updater.win32.ts @@ -6,6 +6,7 @@ 'use strict'; import * as path from 'path'; +import * as fs from 'fs'; import * as pfs from 'vs/base/node/pfs'; import { checksum } from 'vs/base/node/crypto'; import { EventEmitter } from 'events'; @@ -17,6 +18,7 @@ import { download, asJson } from 'vs/base/node/request'; import { IRequestService } from 'vs/platform/request/node/request'; import { IAutoUpdater } from 'vs/platform/update/common/update'; import product from 'vs/platform/node/product'; +import { isActive } from 'windows-mutex'; interface IUpdate { url: string; @@ -25,13 +27,35 @@ interface IUpdate { version: string; productVersion: string; hash: string; + supportsFastUpdate?: boolean; +} + +function pollUntil(fn: () => boolean, timeout = 1000): TPromise { + return new TPromise(c => { + const poll = () => { + if (fn()) { + c(null); + } else { + setTimeout(poll, timeout); + } + }; + + poll(); + }); +} + +interface IAvailableUpdate { + packagePath: string; + version: string; + supportsFastUpdate: boolean; + updateFilePath?: string; } export class Win32AutoUpdaterImpl extends EventEmitter implements IAutoUpdater { private url: string = null; private currentRequest: Promise = null; - private updatePackagePath: string = null; + private currentUpdate: IAvailableUpdate = null; constructor( @IRequestService private requestService: IRequestService @@ -87,14 +111,21 @@ export class Win32AutoUpdaterImpl extends EventEmitter implements IAutoUpdater { .then(() => updatePackagePath); }); }).then(updatePackagePath => { - this.updatePackagePath = updatePackagePath; + const supportsFastUpdate = !!update.supportsFastUpdate; + + this.currentUpdate = { + packagePath: updatePackagePath, + version: update.version, + supportsFastUpdate + }; this.emit('update-downloaded', {}, update.releaseNotes, update.productVersion, new Date(), - this.url + this.url, + supportsFastUpdate ); }); }); @@ -126,12 +157,45 @@ export class Win32AutoUpdaterImpl extends EventEmitter implements IAutoUpdater { ); } + applyUpdate(): TPromise { + if (!this.currentUpdate) { + return TPromise.as(null); + } + + return this.cachePath.then(cachePath => { + this.currentUpdate.updateFilePath = path.join(cachePath, `CodeSetup-${product.quality}-${this.currentUpdate.version}.flag`); + + return pfs.touch(this.currentUpdate.updateFilePath).then(() => { + spawn(this.currentUpdate.packagePath, ['/verysilent', '/update=FILENAME', '/nocloseapplications', '/mergetasks=runcode,!desktopicon,!quicklaunchicon'], { + detached: true, + stdio: ['ignore', 'ignore', 'ignore'] + }); + + const readyMutexName = `${product.win32MutexName}-ready`; + + // poll for mutex-ready + pollUntil(() => isActive(readyMutexName)).then(() => { + + // now we're ready for `quitAndInstall` + this.emit('update-ready'); + }); + }); + }); + } + quitAndInstall(): void { - if (!this.updatePackagePath) { + if (!this.currentUpdate) { return; } - spawn(this.updatePackagePath, ['/silent', '/mergetasks=runcode,!desktopicon,!quicklaunchicon'], { + if (this.currentUpdate.supportsFastUpdate && this.currentUpdate.updateFilePath) { + // let's delete the file, to signal inno setup that we want Code to start + // after the update is applied. after that, just die + fs.unlinkSync(this.currentUpdate.updateFilePath); + return; + } + + spawn(this.currentUpdate.packagePath, ['/silent', '/mergetasks=runcode,!desktopicon,!quicklaunchicon'], { detached: true, stdio: ['ignore', 'ignore', 'ignore'] }); diff --git a/src/vs/platform/update/electron-main/updateService.ts b/src/vs/platform/update/electron-main/updateService.ts index b7f8f5edf69..849f57f279d 100644 --- a/src/vs/platform/update/electron-main/updateService.ts +++ b/src/vs/platform/update/electron-main/updateService.ts @@ -45,6 +45,12 @@ export class UpdateService implements IUpdateService { private _onUpdateNotAvailable = new Emitter(); get onUpdateNotAvailable(): Event { return this._onUpdateNotAvailable.event; } + private _onUpdateDownloaded = new Emitter(); + get onUpdateDownloaded(): Event { return this._onUpdateDownloaded.event; } + + private _onUpdateInstalling = new Emitter(); + get onUpdateInstalling(): Event { return this._onUpdateInstalling.event; } + private _onUpdateReady = new Emitter(); get onUpdateReady(): Event { return this._onUpdateReady.event; } @@ -68,14 +74,19 @@ export class UpdateService implements IUpdateService { @memoize private get onRawUpdateDownloaded(): Event { - return fromNodeEventEmitter(this.raw, 'update-downloaded', (_, releaseNotes, version, date, url) => ({ releaseNotes, version, date })); + return fromNodeEventEmitter(this.raw, 'update-downloaded', (_, releaseNotes, version, date, url, supportsFastUpdate) => ({ releaseNotes, version, date, supportsFastUpdate })); + } + + @memoize + private get onRawUpdateReady(): Event { + return fromNodeEventEmitter(this.raw, 'update-ready'); } get state(): State { return this._state; } - set state(state: State) { + private updateState(state: State): void { this._state = state; this._onStateChange.fire(state); } @@ -119,7 +130,7 @@ export class UpdateService implements IUpdateService { return; // application not signed } - this.state = State.Idle; + this.updateState(State.Idle); // Start checking for updates after 30 seconds this.scheduleCheckForUpdates(30 * 1000) @@ -157,20 +168,20 @@ export class UpdateService implements IUpdateService { } this._onCheckForUpdate.fire(); - this.state = State.CheckingForUpdate; + this.updateState(State.CheckingForUpdate); const listeners: IDisposable[] = []; const result = new TPromise((c, e) => { once(this.onRawError)(e, null, listeners); once(this.onRawUpdateNotAvailable)(() => c(null), null, listeners); once(this.onRawUpdateAvailable)(({ url, version }) => url && c({ url, version }), null, listeners); - once(this.onRawUpdateDownloaded)(({ version, date, releaseNotes }) => c({ version, date, releaseNotes }), null, listeners); + once(this.onRawUpdateDownloaded)(({ version, date, releaseNotes, supportsFastUpdate }) => c({ version, date, releaseNotes, supportsFastUpdate }), null, listeners); this.raw.checkForUpdates(); }).then(update => { if (!update) { this._onUpdateNotAvailable.fire(explicit); - this.state = State.Idle; + this.updateState(State.Idle); /* __GDPR__ "update:notAvailable" : { "explicit" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } @@ -178,6 +189,7 @@ export class UpdateService implements IUpdateService { */ this.telemetryService.publicLog('update:notAvailable', { explicit }); + // LINUX } else if (update.url) { const data: IUpdate = { url: update.url, @@ -188,7 +200,7 @@ export class UpdateService implements IUpdateService { this._availableUpdate = data; this._onUpdateAvailable.fire({ url: update.url, version: update.version }); - this.state = State.UpdateAvailable; + this.updateState(State.UpdateAvailable); /* __GDPR__ "update:available" : { "explicit" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, @@ -202,12 +214,20 @@ export class UpdateService implements IUpdateService { const data: IRawUpdate = { releaseNotes: update.releaseNotes, version: update.version, - date: update.date + date: update.date, + supportsFastUpdate: update.supportsFastUpdate }; this._availableUpdate = data; - this._onUpdateReady.fire(data); - this.state = State.UpdateDownloaded; + + if (update.supportsFastUpdate) { + this._onUpdateDownloaded.fire(data); + this.updateState(State.UpdateDownloaded); + } else { + this._onUpdateReady.fire(data); + this.updateState(State.UpdateReady); + } + /* __GDPR__ "update:downloaded" : { "version" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } @@ -218,7 +238,7 @@ export class UpdateService implements IUpdateService { return update; }, err => { - this.state = State.Idle; + this.updateState(State.Idle); return TPromise.wrapError(err); }); @@ -260,6 +280,26 @@ export class UpdateService implements IUpdateService { return process.platform; } + // for windows fast updates + applyUpdate(): TPromise { + if (this.state !== State.UpdateDownloaded) { + return TPromise.as(null); + } + + if (!this.raw.applyUpdate) { + return TPromise.as(null); + } + + once(this.onRawUpdateReady)(() => { + this._onUpdateReady.fire(this._availableUpdate as IRawUpdate); + this.updateState(State.UpdateReady); + }); + + this._onUpdateInstalling.fire(this._availableUpdate as IRawUpdate); + this.updateState(State.UpdateInstalling); + return this.raw.applyUpdate(); + } + quitAndInstall(): TPromise { if (!this._availableUpdate) { return TPromise.as(null); diff --git a/src/vs/workbench/parts/update/electron-browser/update.ts b/src/vs/workbench/parts/update/electron-browser/update.ts index fca8ccd73a6..08349ed005d 100644 --- a/src/vs/workbench/parts/update/electron-browser/update.ts +++ b/src/vs/workbench/parts/update/electron-browser/update.ts @@ -9,7 +9,7 @@ import nls = require('vs/nls'); import severity from 'vs/base/common/severity'; import { TPromise } from 'vs/base/common/winjs.base'; import { IAction, Action } from 'vs/base/common/actions'; -import { mapEvent } from 'vs/base/common/event'; +import { mapEvent, filterEvent, once } from 'vs/base/common/event'; import { IDisposable, dispose, empty as EmptyDisposable } from 'vs/base/common/lifecycle'; import { Separator } from 'vs/base/browser/ui/actionbar/actionbar'; import { IMessageService, CloseAction, Severity } from 'vs/platform/message/common/message'; @@ -34,16 +34,6 @@ import * as semver from 'semver'; import { OS, isLinux, isWindows } from 'vs/base/common/platform'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -class ApplyUpdateAction extends Action { - constructor( @IUpdateService private updateService: IUpdateService) { - super('update.applyUpdate', nls.localize('updateNow', "Update Now"), null, true); - } - - run(): TPromise { - return this.updateService.quitAndInstall(); - } -} - const NotNowAction = new Action( 'update.later', nls.localize('later', "Later"), @@ -178,17 +168,6 @@ export class ShowCurrentReleaseNotesAction extends AbstractShowReleaseNotesActio } } -export class DownloadAction extends Action { - - constructor( @IUpdateService private updateService: IUpdateService) { - super('update.download', nls.localize('downloadNow', "Download Now"), null, true); - } - - run(): TPromise { - return this.updateService.quitAndInstall(); - } -} - const LinkAction = (id: string, message: string, licenseUrl: string) => new Action( id, message, null, true, () => { window.open(licenseUrl); return TPromise.as(null); } @@ -328,11 +307,25 @@ export class UpdateContribution implements IGlobalActivity { @IWorkbenchEditorService editorService: IWorkbenchEditorService, @IActivityService private activityService: IActivityService ) { - const onUpdateAvailable = isLinux - ? mapEvent(updateService.onUpdateAvailable, e => e.version) - : mapEvent(updateService.onUpdateReady, e => e.version); + if (isLinux) { + mapEvent(updateService.onUpdateAvailable, e => e.version) + (this.onUpdateAvailable, this, this.disposables); + } else if (isWindows) { + // fast updates + mapEvent(updateService.onUpdateDownloaded, e => e.version) + (this.onUpdateDownloaded, this, this.disposables); + mapEvent(updateService.onUpdateInstalling, e => e.version) + (this.onUpdateInstalling, this, this.disposables); + + // regular old updates + mapEvent(filterEvent(updateService.onUpdateReady, e => !e.supportsFastUpdate), e => e.version) + (this.onUpdateAvailable, this, this.disposables); + + } else { + mapEvent(updateService.onUpdateReady, e => e.version) + (this.onUpdateAvailable, this, this.disposables); + } - onUpdateAvailable(this.onUpdateAvailable, this, this.disposables); updateService.onError(this.onError, this, this.disposables); updateService.onUpdateNotAvailable(this.onUpdateNotAvailable, this, this.disposables); @@ -370,7 +363,7 @@ export class UpdateContribution implements IGlobalActivity { } } - private onUpdateAvailable(version: string): void { + private shouldShowNotification(): boolean { const currentVersion = product.commit; const currentMillis = new Date().getTime(); const lastKnownVersion = this.storageService.get('update/lastKnownVersion', StorageScope.GLOBAL); @@ -384,27 +377,73 @@ export class UpdateContribution implements IGlobalActivity { const updateNotificationMillis = this.storageService.getInteger('update/updateNotificationTime', StorageScope.GLOBAL, currentMillis); const diffDays = (currentMillis - updateNotificationMillis) / (1000 * 60 * 60 * 24); - // if 5 days have passed from stored date, show message service - if (diffDays > 5) { - this.showUpdateNotification(version); - } + return diffDays > 5; } - private showUpdateNotification(version: string): void { + // windows fast updates + private onUpdateDownloaded(version: string): void { + if (!this.shouldShowNotification()) { + return; + } + + const releaseNotesAction = this.instantiationService.createInstance(ShowReleaseNotesAction, version); + const installUpdateAction = new Action('update.applyUpdate', nls.localize('installUpdate', "Install Update"), undefined, true, () => { + once(mapEvent(filterEvent(this.updateService.onUpdateReady, e => e.supportsFastUpdate), e => e.version)) + (this.onWindowsFastUpdateReady, this); + + return this.updateService.applyUpdate(); + }); + + this.messageService.show(severity.Info, { + message: nls.localize('updateAvailable', "There's an available update: {0} {1}", product.nameLong, version), + actions: [installUpdateAction, NotNowAction, releaseNotesAction] + }); + } + + // windows fast updates + private onWindowsFastUpdateReady(version: string): void { + const releaseNotesAction = this.instantiationService.createInstance(ShowReleaseNotesAction, version); + const restartAction = new Action('update.applyUpdate', nls.localize('updateNow', "Update Now"), undefined, true, () => this.updateService.quitAndInstall()); + + this.messageService.show(severity.Info, { + message: nls.localize('updateAvailableAfterRestart', "{0} will be updated after it restarts.", product.nameLong), + actions: [restartAction, NotNowAction, releaseNotesAction] + }); + } + + // windows fast updates + private onUpdateInstalling(version: string): void { + const neverShowAgain = new NeverShowAgain('update/win32-fast-updates', this.storageService); + + if (!neverShowAgain.shouldShow()) { + return; + } + + this.messageService.show(severity.Info, { + message: nls.localize('updateInstalling', "{0} {1} is being installed in the background, we'll let you know when it's done.", product.nameLong, version), + actions: [CloseAction, neverShowAgain.action] + }); + } + + private onUpdateAvailable(version: string): void { + if (!this.shouldShowNotification()) { + return; + } + const releaseNotesAction = this.instantiationService.createInstance(ShowReleaseNotesAction, version); if (isLinux) { - const downloadAction = this.instantiationService.createInstance(DownloadAction); + const downloadAction = new Action('update.download', nls.localize('downloadNow', "Download Now"), undefined, true, () => this.updateService.quitAndInstall()); this.messageService.show(severity.Info, { message: nls.localize('thereIsUpdateAvailable', "There is an available update."), actions: [downloadAction, NotNowAction, releaseNotesAction] }); } else { - const applyUpdateAction = this.instantiationService.createInstance(ApplyUpdateAction); + const applyUpdateAction = new Action('update.applyUpdate', nls.localize('updateNow', "Update Now"), undefined, true, () => this.updateService.quitAndInstall()); this.messageService.show(severity.Info, { - message: nls.localize('updateAvailable', "{0} will be updated after it restarts.", product.nameLong), + message: nls.localize('updateAvailableAfterRestart', "{0} will be updated after it restarts.", product.nameLong), actions: [applyUpdateAction, NotNowAction, releaseNotesAction] }); } @@ -461,6 +500,13 @@ export class UpdateContribution implements IGlobalActivity { return new Action('update.available', updateAvailableLabel, undefined, false); case UpdateState.UpdateDownloaded: + return new Action('update.apply', nls.localize('installUpdate...', "Install Update..."), undefined, true, () => + this.updateService.applyUpdate()); + + case UpdateState.UpdateInstalling: + return new Action('update.applying', nls.localize('installingUpdate', "Installing Update..."), undefined, false); + + case UpdateState.UpdateReady: return new Action('update.restart', nls.localize('restartToUpdate', "Restart to Update..."), undefined, true, () => this.updateService.quitAndInstall()); From 079899193e25b7e45f73f6727189f236680a1823 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Fri, 19 Jan 2018 11:48:08 +0100 Subject: [PATCH 013/128] load mutex later --- src/vs/platform/update/electron-main/auto-updater.win32.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/platform/update/electron-main/auto-updater.win32.ts b/src/vs/platform/update/electron-main/auto-updater.win32.ts index 401dc510256..55da62dda4e 100644 --- a/src/vs/platform/update/electron-main/auto-updater.win32.ts +++ b/src/vs/platform/update/electron-main/auto-updater.win32.ts @@ -18,7 +18,6 @@ import { download, asJson } from 'vs/base/node/request'; import { IRequestService } from 'vs/platform/request/node/request'; import { IAutoUpdater } from 'vs/platform/update/common/update'; import product from 'vs/platform/node/product'; -import { isActive } from 'windows-mutex'; interface IUpdate { url: string; @@ -172,6 +171,7 @@ export class Win32AutoUpdaterImpl extends EventEmitter implements IAutoUpdater { }); const readyMutexName = `${product.win32MutexName}-ready`; + const isActive = (require.__$__nodeRequire('windows-mutex') as any).isActive; // poll for mutex-ready pollUntil(() => isActive(readyMutexName)).then(() => { From 7bf655d11a81864ddbdb790ddbd4ecf92cd0cf70 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Fri, 19 Jan 2018 12:32:24 +0100 Subject: [PATCH 014/128] fix badge, win32 flag file --- .../update/electron-main/auto-updater.win32.ts | 10 ++++++++-- .../workbench/parts/update/electron-browser/update.ts | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/vs/platform/update/electron-main/auto-updater.win32.ts b/src/vs/platform/update/electron-main/auto-updater.win32.ts index 55da62dda4e..3100d0941d2 100644 --- a/src/vs/platform/update/electron-main/auto-updater.win32.ts +++ b/src/vs/platform/update/electron-main/auto-updater.win32.ts @@ -164,12 +164,18 @@ export class Win32AutoUpdaterImpl extends EventEmitter implements IAutoUpdater { return this.cachePath.then(cachePath => { this.currentUpdate.updateFilePath = path.join(cachePath, `CodeSetup-${product.quality}-${this.currentUpdate.version}.flag`); - return pfs.touch(this.currentUpdate.updateFilePath).then(() => { - spawn(this.currentUpdate.packagePath, ['/verysilent', '/update=FILENAME', '/nocloseapplications', '/mergetasks=runcode,!desktopicon,!quicklaunchicon'], { + return pfs.writeFile(this.currentUpdate.updateFilePath, 'flag').then(() => { + const child = spawn(this.currentUpdate.packagePath, ['/verysilent', `/update="${this.currentUpdate.updateFilePath}"`, '/nocloseapplications', '/mergetasks=runcode,!desktopicon,!quicklaunchicon'], { detached: true, stdio: ['ignore', 'ignore', 'ignore'] }); + child.once('exit', () => { + this.emit('update-not-available'); + this.currentRequest = null; + this.currentUpdate = null; + }); + const readyMutexName = `${product.win32MutexName}-ready`; const isActive = (require.__$__nodeRequire('windows-mutex') as any).isActive; diff --git a/src/vs/workbench/parts/update/electron-browser/update.ts b/src/vs/workbench/parts/update/electron-browser/update.ts index 08349ed005d..34e36f495bb 100644 --- a/src/vs/workbench/parts/update/electron-browser/update.ts +++ b/src/vs/workbench/parts/update/electron-browser/update.ts @@ -355,7 +355,7 @@ export class UpdateContribution implements IGlobalActivity { const isUpdateAvailable = isLinux ? state === UpdateState.UpdateAvailable - : state === UpdateState.UpdateDownloaded; + : state === UpdateState.UpdateDownloaded || state === UpdateState.UpdateReady; if (isUpdateAvailable) { const badge = new NumberBadge(1, () => nls.localize('updateIsReady', "New {0} update available.", product.nameShort)); From 7032bc2888aac947205e0ad60ecb20d4b9347117 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Fri, 19 Jan 2018 12:33:09 +0100 Subject: [PATCH 015/128] rollback update mechanism if user cancels --- src/vs/platform/update/electron-main/updateService.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/vs/platform/update/electron-main/updateService.ts b/src/vs/platform/update/electron-main/updateService.ts index 849f57f279d..fde9b01d33f 100644 --- a/src/vs/platform/update/electron-main/updateService.ts +++ b/src/vs/platform/update/electron-main/updateService.ts @@ -295,6 +295,11 @@ export class UpdateService implements IUpdateService { this.updateState(State.UpdateReady); }); + once(this.onRawUpdateNotAvailable)(() => { + this._onUpdateNotAvailable.fire(false); + this.updateState(State.Idle); + }); + this._onUpdateInstalling.fire(this._availableUpdate as IRawUpdate); this.updateState(State.UpdateInstalling); return this.raw.applyUpdate(); From f01d2d2d7d912ea42476e0b4da934ed2e819e1df Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Fri, 19 Jan 2018 14:24:56 +0100 Subject: [PATCH 016/128] log asset --- build/tfs/common/publish.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/build/tfs/common/publish.ts b/build/tfs/common/publish.ts index 8e25b630c3e..ca580ce1b4d 100644 --- a/build/tfs/common/publish.ts +++ b/build/tfs/common/publish.ts @@ -240,6 +240,8 @@ async function publish(commit: string, quality: string, platform: string, type: asset.supportsFastUpdate = true; } + console.log('Asset:', JSON.stringify(asset, null, ' ')); + const release = { id: commit, timestamp: (new Date()).getTime(), From 16306c893a08469857a0156d43688dca4d3190bc Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Fri, 19 Jan 2018 19:25:43 +0100 Subject: [PATCH 017/128] unify update services across platforms --- src/vs/code/electron-main/app.ts | 13 +- src/vs/code/electron-main/menus.ts | 63 ++-- src/vs/platform/update/common/update.ts | 67 ++-- src/vs/platform/update/common/updateIpc.ts | 38 +- .../electron-main/abstractUpdateService.ts | 137 +++++++ .../electron-main/auto-updater.linux.ts | 77 ---- .../electron-main/auto-updater.win32.ts | 209 ----------- .../electron-main/updateService.darwin.ts | 114 ++++++ .../electron-main/updateService.linux.ts | 77 ++++ .../update/electron-main/updateService.ts | 340 ------------------ .../electron-main/updateService.win32.ts | 207 +++++++++++ .../parts/update/electron-browser/update.ts | 293 +++++++-------- 12 files changed, 767 insertions(+), 868 deletions(-) create mode 100644 src/vs/platform/update/electron-main/abstractUpdateService.ts delete mode 100644 src/vs/platform/update/electron-main/auto-updater.linux.ts delete mode 100644 src/vs/platform/update/electron-main/auto-updater.win32.ts create mode 100644 src/vs/platform/update/electron-main/updateService.darwin.ts create mode 100644 src/vs/platform/update/electron-main/updateService.linux.ts delete mode 100644 src/vs/platform/update/electron-main/updateService.ts create mode 100644 src/vs/platform/update/electron-main/updateService.win32.ts diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index ca4a569c0f6..d5a8604b0bb 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -16,7 +16,6 @@ import { CodeMenu } from 'vs/code/electron-main/menus'; import { getShellEnvironment } from 'vs/code/node/shellEnv'; import { IUpdateService } from 'vs/platform/update/common/update'; import { UpdateChannel } from 'vs/platform/update/common/updateIpc'; -import { UpdateService } from 'vs/platform/update/electron-main/updateService'; import { Server as ElectronIPCServer } from 'vs/base/parts/ipc/electron-main/ipc.electron-main'; import { Server, connect, Client } from 'vs/base/parts/ipc/node/ipc.net'; import { SharedProcess } from 'vs/code/electron-main/sharedProcess'; @@ -52,6 +51,9 @@ import URI from 'vs/base/common/uri'; import { WorkspacesChannel } from 'vs/platform/workspaces/common/workspacesIpc'; import { IWorkspacesMainService } from 'vs/platform/workspaces/common/workspaces'; import { getMachineId } from 'vs/base/node/id'; +import { Win32UpdateService } from 'vs/platform/update/electron-main/updateService.win32'; +import { LinuxUpdateService } from 'vs/platform/update/electron-main/updateService.linux'; +import { DarwinUpdateService } from 'vs/platform/update/electron-main/updateService.darwin'; export class CodeApplication { @@ -296,7 +298,14 @@ export class CodeApplication { private initServices(machineId: string): IInstantiationService { const services = new ServiceCollection(); - services.set(IUpdateService, new SyncDescriptor(UpdateService)); + if (process.platform === 'win32') { + services.set(IUpdateService, new SyncDescriptor(Win32UpdateService)); + } else if (process.platform === 'linux') { + services.set(IUpdateService, new SyncDescriptor(LinuxUpdateService)); + } else if (process.platform === 'darwin') { + services.set(IUpdateService, new SyncDescriptor(DarwinUpdateService)); + } + services.set(IWindowsMainService, new SyncDescriptor(WindowsManager, machineId)); services.set(IWindowsService, new SyncDescriptor(WindowsService, this.sharedProcess)); services.set(ILaunchService, new SyncDescriptor(LaunchService)); diff --git a/src/vs/code/electron-main/menus.ts b/src/vs/code/electron-main/menus.ts index ced1253651e..6a3d1c1da5f 100644 --- a/src/vs/code/electron-main/menus.ts +++ b/src/vs/code/electron-main/menus.ts @@ -14,7 +14,7 @@ import { OpenContext, IRunActionInWindowRequest } from 'vs/platform/windows/comm import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; import { AutoSaveConfiguration } from 'vs/platform/files/common/files'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { IUpdateService, State as UpdateState } from 'vs/platform/update/common/update'; +import { IUpdateService, StateType } from 'vs/platform/update/common/update'; import product from 'vs/platform/node/product'; import { RunOnceScheduler } from 'vs/base/common/async'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -1040,11 +1040,34 @@ export class CodeMenu { } private getUpdateMenuItems(): Electron.MenuItem[] { - switch (this.updateService.state) { - case UpdateState.Uninitialized: + const state = this.updateService.state; + + switch (state.type) { + case StateType.Uninitialized: return []; - case UpdateState.UpdateDownloaded: + case StateType.Idle: + return [new MenuItem({ + label: nls.localize('miCheckForUpdates', "Check for Updates..."), click: () => setTimeout(() => { + this.reportMenuActionTelemetry('CheckForUpdate'); + this.updateService.checkForUpdates(true); + }, 0) + })]; + + case StateType.CheckingForUpdates: + return [new MenuItem({ label: nls.localize('miCheckingForUpdates', "Checking For Updates..."), enabled: false })]; + + case StateType.Available: + return [new MenuItem({ + label: nls.localize('miDownloadUpdate', "Download Available Update"), click: () => { + shell.openExternal(state.update.url); + } + })]; + + case StateType.Downloading: + return [new MenuItem({ label: nls.localize('miDownloadingUpdate', "Downloading Update..."), enabled: false })]; + + case StateType.Downloaded: return [new MenuItem({ label: nls.localize('miInstallUpdate', "Install Update..."), click: () => { this.reportMenuActionTelemetry('InstallUpdate'); @@ -1052,44 +1075,16 @@ export class CodeMenu { } })]; - case UpdateState.UpdateInstalling: + case StateType.Updating: return [new MenuItem({ label: nls.localize('miInstallingUpdate', "Installing Update..."), enabled: false })]; - case UpdateState.UpdateReady: + case StateType.Ready: return [new MenuItem({ label: nls.localize('miRestartToUpdate', "Restart to Update..."), click: () => { this.reportMenuActionTelemetry('RestartToUpdate'); this.updateService.quitAndInstall(); } })]; - - case UpdateState.CheckingForUpdate: - return [new MenuItem({ label: nls.localize('miCheckingForUpdates', "Checking For Updates..."), enabled: false })]; - - case UpdateState.UpdateAvailable: - if (isLinux) { - return [new MenuItem({ - label: nls.localize('miDownloadUpdate', "Download Available Update"), click: () => { - this.updateService.quitAndInstall(); - } - })]; - } - - const updateAvailableLabel = isWindows - ? nls.localize('miDownloadingUpdate', "Downloading Update...") - : nls.localize('miInstallingUpdate', "Installing Update..."); - - return [new MenuItem({ label: updateAvailableLabel, enabled: false })]; - - default: - const result = [new MenuItem({ - label: nls.localize('miCheckForUpdates', "Check for Updates..."), click: () => setTimeout(() => { - this.reportMenuActionTelemetry('CheckForUpdate'); - this.updateService.checkForUpdates(true); - }, 0) - })]; - - return result; } } diff --git a/src/vs/platform/update/common/update.ts b/src/vs/platform/update/common/update.ts index eecba458299..5a397113d90 100644 --- a/src/vs/platform/update/common/update.ts +++ b/src/vs/platform/update/common/update.ts @@ -9,36 +9,49 @@ import Event, { NodeEventEmitter } from 'vs/base/common/event'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { TPromise } from 'vs/base/common/winjs.base'; -export enum State { - Uninitialized, - Idle, - CheckingForUpdate, - UpdateAvailable, - UpdateDownloaded, - UpdateInstalling, - UpdateReady -} - -export enum ExplicitState { - Implicit, - Explicit -} - -export interface IRawUpdate { - releaseNotes: string; - version: string; - date: Date; - supportsFastUpdate?: boolean; -} - export interface IUpdate { version: string; + productVersion: string; date?: Date; releaseNotes?: string; - url?: string; supportsFastUpdate?: boolean; + url?: string; + hash?: string; } +export enum StateType { + Uninitialized = 'uninitialized', + Idle = 'idle', + Available = 'available', + CheckingForUpdates = 'checking for updates', + Downloading = 'downloading', + Downloaded = 'downloaded', + Updating = 'updating', + Ready = 'ready', +} + +export type Uninitialized = { type: StateType.Uninitialized }; +export type Idle = { type: StateType.Idle }; +export type CheckingForUpdates = { type: StateType.CheckingForUpdates, explicit: boolean }; +export type Available = { type: StateType.Available, update: IUpdate }; +export type Downloading = { type: StateType.Downloading, update: IUpdate }; +export type Downloaded = { type: StateType.Downloaded, update: IUpdate }; +export type Updating = { type: StateType.Updating, update: IUpdate }; +export type Ready = { type: StateType.Ready, update: IUpdate }; + +export type State = Uninitialized | Idle | CheckingForUpdates | Available | Downloading | Downloaded | Updating | Ready; + +export const State = { + Uninitialized: { type: StateType.Uninitialized } as Uninitialized, + Idle: { type: StateType.Idle } as Idle, + CheckingForUpdates: (explicit: boolean) => ({ type: StateType.CheckingForUpdates, explicit } as CheckingForUpdates), + Available: (update: IUpdate) => ({ type: StateType.Available, update } as Available), + Downloading: (update: IUpdate) => ({ type: StateType.Downloading, update } as Downloading), + Downloaded: (update: IUpdate) => ({ type: StateType.Downloaded, update } as Downloaded), + Updating: (update: IUpdate) => ({ type: StateType.Updating, update } as Updating), + Ready: (update: IUpdate) => ({ type: StateType.Ready, update } as Ready), +}; + export interface IAutoUpdater extends NodeEventEmitter { setFeedURL(url: string): void; checkForUpdates(): void; @@ -51,16 +64,10 @@ export const IUpdateService = createDecorator('updateService'); export interface IUpdateService { _serviceBrand: any; - readonly onError: Event; - readonly onUpdateAvailable: Event<{ url: string; version: string; }>; - readonly onUpdateNotAvailable: Event; - readonly onUpdateDownloaded: Event; - readonly onUpdateInstalling: Event; - readonly onUpdateReady: Event; readonly onStateChange: Event; readonly state: State; - checkForUpdates(explicit: boolean): TPromise; + checkForUpdates(explicit: boolean): TPromise; applyUpdate(): TPromise; quitAndInstall(): TPromise; } \ No newline at end of file diff --git a/src/vs/platform/update/common/updateIpc.ts b/src/vs/platform/update/common/updateIpc.ts index 9b8b30041a3..9aef4457e9b 100644 --- a/src/vs/platform/update/common/updateIpc.ts +++ b/src/vs/platform/update/common/updateIpc.ts @@ -9,17 +9,10 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { IChannel, eventToCall, eventFromCall } from 'vs/base/parts/ipc/common/ipc'; import Event, { Emitter } from 'vs/base/common/event'; import { onUnexpectedError } from 'vs/base/common/errors'; -import { IUpdateService, IRawUpdate, State, IUpdate } from './update'; +import { IUpdateService, State } from './update'; export interface IUpdateChannel extends IChannel { - call(command: 'event:onError'): TPromise; - call(command: 'event:onUpdateAvailable'): TPromise; - call(command: 'event:onUpdateNotAvailable'): TPromise; - call(command: 'event:onUpdateDownloaded'): TPromise; - call(command: 'event:onUpdateInstalling'): TPromise; - call(command: 'event:onUpdateReady'): TPromise; - call(command: 'event:onStateChange'): TPromise; - call(command: 'checkForUpdates', arg: boolean): TPromise; + call(command: 'checkForUpdates', arg: boolean): TPromise; call(command: 'applyUpdate'): TPromise; call(command: 'quitAndInstall'): TPromise; call(command: '_getInitialState'): TPromise; @@ -32,12 +25,6 @@ export class UpdateChannel implements IUpdateChannel { call(command: string, arg?: any): TPromise { switch (command) { - case 'event:onError': return eventToCall(this.service.onError); - case 'event:onUpdateAvailable': return eventToCall(this.service.onUpdateAvailable); - case 'event:onUpdateNotAvailable': return eventToCall(this.service.onUpdateNotAvailable); - case 'event:onUpdateDownloaded': return eventToCall(this.service.onUpdateDownloaded); - case 'event:onUpdateInstalling': return eventToCall(this.service.onUpdateInstalling); - case 'event:onUpdateReady': return eventToCall(this.service.onUpdateReady); case 'event:onStateChange': return eventToCall(this.service.onStateChange); case 'checkForUpdates': return this.service.checkForUpdates(arg); case 'applyUpdate': return this.service.applyUpdate(); @@ -52,25 +39,8 @@ export class UpdateChannelClient implements IUpdateService { _serviceBrand: any; - private _onError = eventFromCall(this.channel, 'event:onError'); - get onError(): Event { return this._onError; } - - private _onUpdateAvailable = eventFromCall<{ url: string; version: string; }>(this.channel, 'event:onUpdateAvailable'); - get onUpdateAvailable(): Event<{ url: string; version: string; }> { return this._onUpdateAvailable; } - - private _onUpdateNotAvailable = eventFromCall(this.channel, 'event:onUpdateNotAvailable'); - get onUpdateNotAvailable(): Event { return this._onUpdateNotAvailable; } - - private _onUpdateDownloaded = eventFromCall(this.channel, 'event:onUpdateDownloaded'); - get onUpdateDownloaded(): Event { return this._onUpdateDownloaded; } - - private _onUpdateInstalling = eventFromCall(this.channel, 'event:onUpdateInstalling'); - get onUpdateInstalling(): Event { return this._onUpdateInstalling; } - - private _onUpdateReady = eventFromCall(this.channel, 'event:onUpdateReady'); - get onUpdateReady(): Event { return this._onUpdateReady; } - private _onRemoteStateChange = eventFromCall(this.channel, 'event:onStateChange'); + private _onStateChange = new Emitter(); get onStateChange(): Event { return this._onStateChange.event; } @@ -90,7 +60,7 @@ export class UpdateChannelClient implements IUpdateService { }, onUnexpectedError); } - checkForUpdates(explicit: boolean): TPromise { + checkForUpdates(explicit: boolean): TPromise { return this.channel.call('checkForUpdates', explicit); } diff --git a/src/vs/platform/update/electron-main/abstractUpdateService.ts b/src/vs/platform/update/electron-main/abstractUpdateService.ts new file mode 100644 index 00000000000..f7fa618119c --- /dev/null +++ b/src/vs/platform/update/electron-main/abstractUpdateService.ts @@ -0,0 +1,137 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import Event, { Emitter } from 'vs/base/common/event'; +import { Throttler } from 'vs/base/common/async'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ILifecycleService } from 'vs/platform/lifecycle/electron-main/lifecycleMain'; +import product from 'vs/platform/node/product'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { IUpdateService, State, StateType } from 'vs/platform/update/common/update'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { ILogService } from 'vs/platform/log/common/log'; + +export function createUpdateURL(platform: string, quality: string): string { + return `${product.updateUrl}/api/update/${platform}/${quality}/${product.commit}`; +} + +export abstract class AbstractUpdateService implements IUpdateService { + + _serviceBrand: any; + + private _state: State = State.Uninitialized; + private throttler: Throttler = new Throttler(); + + private _onStateChange = new Emitter(); + get onStateChange(): Event { return this._onStateChange.event; } + + get state(): State { + return this._state; + } + + protected setState(state: State): void { + this._state = state; + this._onStateChange.fire(state); + } + + constructor( + @ILifecycleService private lifecycleService: ILifecycleService, + @IConfigurationService private configurationService: IConfigurationService, + @IEnvironmentService private environmentService: IEnvironmentService, + @ILogService protected logService: ILogService + ) { + if (this.environmentService.disableUpdates) { + return; + } + + if (!product.updateUrl || !product.commit) { + return; + } + + const quality = this.getProductQuality(); + + if (!quality) { + return; + } + + if (!this.setUpdateFeedUrl(quality)) { + return; + } + + this.setState({ type: StateType.Idle }); + + // Start checking for updates after 30 seconds + this.scheduleCheckForUpdates(30 * 1000) + .done(null, err => this.logService.error(err)); + } + + private getProductQuality(): string { + const quality = this.configurationService.getValue('update.channel'); + return quality === 'none' ? null : product.quality; + } + + private scheduleCheckForUpdates(delay = 60 * 60 * 1000): TPromise { + return TPromise.timeout(delay) + .then(() => this.checkForUpdates()) + .then(update => { + if (update) { + // Update found, no need to check more + return TPromise.as(null); + } + + // Check again after 1 hour + return this.scheduleCheckForUpdates(60 * 60 * 1000); + }); + } + + checkForUpdates(explicit = false): TPromise { + if (this.state !== State.Idle) { + return TPromise.as(null); + } + + return this.throttler.queue(() => TPromise.as(this.doCheckForUpdates(explicit))); + } + + applyUpdate(): TPromise { + if (this.state.type !== StateType.Ready) { + return TPromise.as(null); + } + + return this.doApplyUpdate(); + } + + protected doApplyUpdate(): TPromise { + return TPromise.as(null); + } + + quitAndInstall(): TPromise { + if (this.state.type !== StateType.Ready) { + return TPromise.as(null); + } + + this.logService.trace('update#quitAndInstall(): before lifecycle quit()'); + + this.lifecycleService.quit(true /* from update */).done(vetod => { + this.logService.trace(`update#quitAndInstall(): after lifecycle quit() with veto: ${vetod}`); + if (vetod) { + return; + } + + this.logService.trace('update#quitAndInstall(): running raw#quitAndInstall()'); + this.doQuitAndInstall(); + }); + + return TPromise.as(null); + } + + protected doQuitAndInstall(): void { + // noop + } + + protected abstract setUpdateFeedUrl(quality: string): boolean; + protected abstract doCheckForUpdates(explicit: boolean): void; +} diff --git a/src/vs/platform/update/electron-main/auto-updater.linux.ts b/src/vs/platform/update/electron-main/auto-updater.linux.ts deleted file mode 100644 index bdc9c183329..00000000000 --- a/src/vs/platform/update/electron-main/auto-updater.linux.ts +++ /dev/null @@ -1,77 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; - -import { EventEmitter } from 'events'; -import { isString } from 'vs/base/common/types'; -import { Promise } from 'vs/base/common/winjs.base'; -import { asJson } from 'vs/base/node/request'; -import { IRequestService } from 'vs/platform/request/node/request'; -import { IAutoUpdater } from 'vs/platform/update/common/update'; -import product from 'vs/platform/node/product'; - -interface IUpdate { - url: string; - name: string; - releaseNotes?: string; - version: string; - productVersion: string; - hash: string; -} - -export class LinuxAutoUpdaterImpl extends EventEmitter implements IAutoUpdater { - - private url: string; - private currentRequest: Promise; - - constructor( - @IRequestService private requestService: IRequestService - ) { - super(); - - this.url = null; - this.currentRequest = null; - } - - setFeedURL(url: string): void { - this.url = url; - } - - checkForUpdates(): void { - if (!this.url) { - throw new Error('No feed url set.'); - } - - if (this.currentRequest) { - return; - } - - this.emit('checking-for-update'); - - this.currentRequest = this.requestService.request({ url: this.url }) - .then(asJson) - .then(update => { - if (!update || !update.url || !update.version || !update.productVersion) { - this.emit('update-not-available'); - } else { - this.emit('update-available', null, product.downloadUrl, update.productVersion); - } - }) - .then(null, e => { - if (isString(e) && /^Server returned/.test(e)) { - return; - } - - this.emit('update-not-available'); - this.emit('error', e); - }) - .then(() => this.currentRequest = null); - } - - quitAndInstall(): void { - // noop - } -} diff --git a/src/vs/platform/update/electron-main/auto-updater.win32.ts b/src/vs/platform/update/electron-main/auto-updater.win32.ts deleted file mode 100644 index 3100d0941d2..00000000000 --- a/src/vs/platform/update/electron-main/auto-updater.win32.ts +++ /dev/null @@ -1,209 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; - -import * as path from 'path'; -import * as fs from 'fs'; -import * as pfs from 'vs/base/node/pfs'; -import { checksum } from 'vs/base/node/crypto'; -import { EventEmitter } from 'events'; -import { tmpdir } from 'os'; -import { spawn } from 'child_process'; -import { isString } from 'vs/base/common/types'; -import { Promise, TPromise } from 'vs/base/common/winjs.base'; -import { download, asJson } from 'vs/base/node/request'; -import { IRequestService } from 'vs/platform/request/node/request'; -import { IAutoUpdater } from 'vs/platform/update/common/update'; -import product from 'vs/platform/node/product'; - -interface IUpdate { - url: string; - name: string; - releaseNotes?: string; - version: string; - productVersion: string; - hash: string; - supportsFastUpdate?: boolean; -} - -function pollUntil(fn: () => boolean, timeout = 1000): TPromise { - return new TPromise(c => { - const poll = () => { - if (fn()) { - c(null); - } else { - setTimeout(poll, timeout); - } - }; - - poll(); - }); -} - -interface IAvailableUpdate { - packagePath: string; - version: string; - supportsFastUpdate: boolean; - updateFilePath?: string; -} - -export class Win32AutoUpdaterImpl extends EventEmitter implements IAutoUpdater { - - private url: string = null; - private currentRequest: Promise = null; - private currentUpdate: IAvailableUpdate = null; - - constructor( - @IRequestService private requestService: IRequestService - ) { - super(); - } - - get cachePath(): TPromise { - const result = path.join(tmpdir(), `vscode-update-${process.arch}`); - return pfs.mkdirp(result, null).then(() => result); - } - - setFeedURL(url: string): void { - this.url = url; - } - - checkForUpdates(): void { - if (!this.url) { - throw new Error('No feed url set.'); - } - - if (this.currentRequest) { - return; - } - - this.emit('checking-for-update'); - - this.currentRequest = this.requestService.request({ url: this.url }) - .then(asJson) - .then(update => { - if (!update || !update.url || !update.version) { - this.emit('update-not-available'); - return this.cleanup(); - } - - this.emit('update-available'); - - return this.cleanup(update.version).then(() => { - return this.getUpdatePackagePath(update.version).then(updatePackagePath => { - return pfs.exists(updatePackagePath).then(exists => { - if (exists) { - return TPromise.as(updatePackagePath); - } - - const url = update.url; - const hash = update.hash; - const downloadPath = `${updatePackagePath}.tmp`; - - return this.requestService.request({ url }) - .then(context => download(downloadPath, context)) - .then(hash ? () => checksum(downloadPath, update.hash) : () => null) - .then(() => pfs.rename(downloadPath, updatePackagePath)) - .then(() => updatePackagePath); - }); - }).then(updatePackagePath => { - const supportsFastUpdate = !!update.supportsFastUpdate; - - this.currentUpdate = { - packagePath: updatePackagePath, - version: update.version, - supportsFastUpdate - }; - - this.emit('update-downloaded', - {}, - update.releaseNotes, - update.productVersion, - new Date(), - this.url, - supportsFastUpdate - ); - }); - }); - }) - .then(null, e => { - if (isString(e) && /^Server returned/.test(e)) { - return; - } - - this.emit('update-not-available'); - this.emit('error', e); - }) - .then(() => this.currentRequest = null); - } - - private getUpdatePackagePath(version: string): TPromise { - return this.cachePath.then(cachePath => path.join(cachePath, `CodeSetup-${product.quality}-${version}.exe`)); - } - - private cleanup(exceptVersion: string = null): Promise { - const filter = exceptVersion ? one => !(new RegExp(`${product.quality}-${exceptVersion}\\.exe$`).test(one)) : () => true; - - return this.cachePath - .then(cachePath => pfs.readdir(cachePath) - .then(all => Promise.join(all - .filter(filter) - .map(one => pfs.unlink(path.join(cachePath, one)).then(null, () => null)) - )) - ); - } - - applyUpdate(): TPromise { - if (!this.currentUpdate) { - return TPromise.as(null); - } - - return this.cachePath.then(cachePath => { - this.currentUpdate.updateFilePath = path.join(cachePath, `CodeSetup-${product.quality}-${this.currentUpdate.version}.flag`); - - return pfs.writeFile(this.currentUpdate.updateFilePath, 'flag').then(() => { - const child = spawn(this.currentUpdate.packagePath, ['/verysilent', `/update="${this.currentUpdate.updateFilePath}"`, '/nocloseapplications', '/mergetasks=runcode,!desktopicon,!quicklaunchicon'], { - detached: true, - stdio: ['ignore', 'ignore', 'ignore'] - }); - - child.once('exit', () => { - this.emit('update-not-available'); - this.currentRequest = null; - this.currentUpdate = null; - }); - - const readyMutexName = `${product.win32MutexName}-ready`; - const isActive = (require.__$__nodeRequire('windows-mutex') as any).isActive; - - // poll for mutex-ready - pollUntil(() => isActive(readyMutexName)).then(() => { - - // now we're ready for `quitAndInstall` - this.emit('update-ready'); - }); - }); - }); - } - - quitAndInstall(): void { - if (!this.currentUpdate) { - return; - } - - if (this.currentUpdate.supportsFastUpdate && this.currentUpdate.updateFilePath) { - // let's delete the file, to signal inno setup that we want Code to start - // after the update is applied. after that, just die - fs.unlinkSync(this.currentUpdate.updateFilePath); - return; - } - - spawn(this.currentUpdate.packagePath, ['/silent', '/mergetasks=runcode,!desktopicon,!quicklaunchicon'], { - detached: true, - stdio: ['ignore', 'ignore', 'ignore'] - }); - } -} diff --git a/src/vs/platform/update/electron-main/updateService.darwin.ts b/src/vs/platform/update/electron-main/updateService.darwin.ts new file mode 100644 index 00000000000..88b0f193102 --- /dev/null +++ b/src/vs/platform/update/electron-main/updateService.darwin.ts @@ -0,0 +1,114 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as electron from 'electron'; +import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import Event, { fromNodeEventEmitter } from 'vs/base/common/event'; +import { memoize } from 'vs/base/common/decorators'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ILifecycleService } from 'vs/platform/lifecycle/electron-main/lifecycleMain'; +import { State, IUpdate, StateType } from 'vs/platform/update/common/update'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { ILogService } from 'vs/platform/log/common/log'; +import { AbstractUpdateService, createUpdateURL } from 'vs/platform/update/electron-main/abstractUpdateService'; + +export class DarwinUpdateService extends AbstractUpdateService { + + _serviceBrand: any; + + private disposables: IDisposable[] = []; + + @memoize private get onRawError(): Event { return fromNodeEventEmitter(electron.autoUpdater, 'error', (_, message) => message); } + @memoize private get onRawUpdateNotAvailable(): Event { return fromNodeEventEmitter(electron.autoUpdater, 'update-not-available'); } + @memoize private get onRawUpdateAvailable(): Event { return fromNodeEventEmitter(electron.autoUpdater, 'update-available', (_, url, version) => ({ url, version, productVersion: version })); } + @memoize private get onRawUpdateDownloaded(): Event { return fromNodeEventEmitter(electron.autoUpdater, 'update-downloaded', (_, releaseNotes, version, date) => ({ releaseNotes, version, productVersion: version, date })); } + + constructor( + @ILifecycleService lifecycleService: ILifecycleService, + @IConfigurationService configurationService: IConfigurationService, + @ITelemetryService private telemetryService: ITelemetryService, + @IEnvironmentService environmentService: IEnvironmentService, + @ILogService logService: ILogService + ) { + super(lifecycleService, configurationService, environmentService, logService); + this.onRawError(this.logService.error, this.logService, this.disposables); + this.onRawUpdateAvailable(this.onUpdateAvailable, this, this.disposables); + this.onRawUpdateDownloaded(this.onUpdateDownloaded, this, this.disposables); + this.onRawUpdateNotAvailable(this.onUpdateNotAvailable, this, this.disposables); + } + + protected setUpdateFeedUrl(quality: string): boolean { + try { + electron.autoUpdater.setFeedURL(createUpdateURL('darwin', quality)); + } catch (e) { + // application is very likely not signed + this.logService.error('Failed to set update feed URL'); + return false; + } + + return true; + } + + protected doCheckForUpdates(explicit: boolean): void { + this.setState(State.CheckingForUpdates(explicit)); + electron.autoUpdater.checkForUpdates(); + } + + private onUpdateAvailable(update: IUpdate): void { + if (this.state.type !== StateType.CheckingForUpdates) { + return; + } + + this.setState(State.Downloading(update)); + } + + private onUpdateDownloaded(update: IUpdate): void { + if (this.state.type !== StateType.Downloading) { + return; + } + + /* __GDPR__ + "update:downloaded" : { + "version" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } + */ + this.telemetryService.publicLog('update:downloaded', { version: update.version }); + + this.setState(State.Ready(update)); + } + + private onUpdateNotAvailable(): void { + if (this.state.type !== StateType.CheckingForUpdates) { + return; + } + + /* __GDPR__ + "update:notAvailable" : { + "explicit" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } + */ + this.telemetryService.publicLog('update:notAvailable', { explicit: this.state.explicit }); + + this.setState(State.Idle); + } + + protected doQuitAndInstall(): void { + // for some reason updating on Mac causes the local storage not to be flushed. + // we workaround this issue by forcing an explicit flush of the storage data. + // see also https://github.com/Microsoft/vscode/issues/172 + this.logService.trace('update#quitAndInstall(): calling flushStorageData()'); + electron.session.defaultSession.flushStorageData(); + + this.logService.trace('update#quitAndInstall(): running raw#quitAndInstall()'); + electron.autoUpdater.quitAndInstall(); + } + + dispose(): void { + this.disposables = dispose(this.disposables); + } +} diff --git a/src/vs/platform/update/electron-main/updateService.linux.ts b/src/vs/platform/update/electron-main/updateService.linux.ts new file mode 100644 index 00000000000..a8c1ea132b2 --- /dev/null +++ b/src/vs/platform/update/electron-main/updateService.linux.ts @@ -0,0 +1,77 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ILifecycleService } from 'vs/platform/lifecycle/electron-main/lifecycleMain'; +import { IRequestService } from 'vs/platform/request/node/request'; +import { State, IUpdate } from 'vs/platform/update/common/update'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { ILogService } from 'vs/platform/log/common/log'; +import { createUpdateURL, AbstractUpdateService } from 'vs/platform/update/electron-main/abstractUpdateService'; +import { asJson } from 'vs/base/node/request'; + +export class LinuxUpdateService extends AbstractUpdateService { + + _serviceBrand: any; + + private url: string | undefined; + + constructor( + @ILifecycleService lifecycleService: ILifecycleService, + @IConfigurationService configurationService: IConfigurationService, + @ITelemetryService private telemetryService: ITelemetryService, + @IEnvironmentService environmentService: IEnvironmentService, + @IRequestService private requestService: IRequestService, + @ILogService logService: ILogService + ) { + super(lifecycleService, configurationService, environmentService, logService); + } + + protected setUpdateFeedUrl(quality: string): boolean { + this.url = createUpdateURL(`linux-${process.arch}`, quality); + return true; + } + + protected doCheckForUpdates(explicit: boolean): void { + if (!this.url) { + return; + } + + this.requestService.request({ url: this.url }) + .then(asJson) + .then(update => { + if (!update || !update.url || !update.version || !update.productVersion) { + /* __GDPR__ + "update:notAvailable" : { + "explicit" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } + */ + this.telemetryService.publicLog('update:notAvailable', { explicit }); + + this.setState(State.Idle); + } else { + this.setState(State.Available(update)); + } + }) + .then(null, err => { + this.logService.error(err); + + /* __GDPR__ + "update:notAvailable" : { + "explicit" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } + */ + this.telemetryService.publicLog('update:notAvailable', { explicit }); + this.setState(State.Idle); + }); + } + + protected doQuitAndInstall(): void { + // not available + } +} diff --git a/src/vs/platform/update/electron-main/updateService.ts b/src/vs/platform/update/electron-main/updateService.ts deleted file mode 100644 index fde9b01d33f..00000000000 --- a/src/vs/platform/update/electron-main/updateService.ts +++ /dev/null @@ -1,340 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; - -import * as fs from 'original-fs'; -import * as path from 'path'; -import * as electron from 'electron'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import Event, { Emitter, once, filterEvent, fromNodeEventEmitter } from 'vs/base/common/event'; -import { always, Throttler } from 'vs/base/common/async'; -import { memoize } from 'vs/base/common/decorators'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { Win32AutoUpdaterImpl } from './auto-updater.win32'; -import { LinuxAutoUpdaterImpl } from './auto-updater.linux'; -import { ILifecycleService } from 'vs/platform/lifecycle/electron-main/lifecycleMain'; -import { IRequestService } from 'vs/platform/request/node/request'; -import product from 'vs/platform/node/product'; -import { TPromise } from 'vs/base/common/winjs.base'; -import { IUpdateService, State, IAutoUpdater, IUpdate, IRawUpdate } from 'vs/platform/update/common/update'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { ILogService } from 'vs/platform/log/common/log'; - -export class UpdateService implements IUpdateService { - - _serviceBrand: any; - - private _state: State = State.Uninitialized; - private _availableUpdate: IUpdate = null; - private raw: IAutoUpdater; - private throttler: Throttler = new Throttler(); - - private _onError = new Emitter(); - get onError(): Event { return this._onError.event; } - - private _onCheckForUpdate = new Emitter(); - get onCheckForUpdate(): Event { return this._onCheckForUpdate.event; } - - private _onUpdateAvailable = new Emitter<{ url: string; version: string; }>(); - get onUpdateAvailable(): Event<{ url: string; version: string; }> { return this._onUpdateAvailable.event; } - - private _onUpdateNotAvailable = new Emitter(); - get onUpdateNotAvailable(): Event { return this._onUpdateNotAvailable.event; } - - private _onUpdateDownloaded = new Emitter(); - get onUpdateDownloaded(): Event { return this._onUpdateDownloaded.event; } - - private _onUpdateInstalling = new Emitter(); - get onUpdateInstalling(): Event { return this._onUpdateInstalling.event; } - - private _onUpdateReady = new Emitter(); - get onUpdateReady(): Event { return this._onUpdateReady.event; } - - private _onStateChange = new Emitter(); - get onStateChange(): Event { return this._onStateChange.event; } - - @memoize - private get onRawError(): Event { - return fromNodeEventEmitter(this.raw, 'error', (_, message) => message); - } - - @memoize - private get onRawUpdateNotAvailable(): Event { - return fromNodeEventEmitter(this.raw, 'update-not-available'); - } - - @memoize - private get onRawUpdateAvailable(): Event<{ url: string; version: string; }> { - return filterEvent(fromNodeEventEmitter(this.raw, 'update-available', (_, url, version) => ({ url, version })), ({ url }) => !!url); - } - - @memoize - private get onRawUpdateDownloaded(): Event { - return fromNodeEventEmitter(this.raw, 'update-downloaded', (_, releaseNotes, version, date, url, supportsFastUpdate) => ({ releaseNotes, version, date, supportsFastUpdate })); - } - - @memoize - private get onRawUpdateReady(): Event { - return fromNodeEventEmitter(this.raw, 'update-ready'); - } - - get state(): State { - return this._state; - } - - private updateState(state: State): void { - this._state = state; - this._onStateChange.fire(state); - } - - get availableUpdate(): IUpdate { - return this._availableUpdate; - } - - constructor( - @IRequestService requestService: IRequestService, - @ILifecycleService private lifecycleService: ILifecycleService, - @IConfigurationService private configurationService: IConfigurationService, - @ITelemetryService private telemetryService: ITelemetryService, - @IEnvironmentService private environmentService: IEnvironmentService, - @ILogService private logService: ILogService - ) { - if (process.platform === 'win32') { - this.raw = new Win32AutoUpdaterImpl(requestService); - } else if (process.platform === 'linux') { - this.raw = new LinuxAutoUpdaterImpl(requestService); - } else if (process.platform === 'darwin') { - this.raw = electron.autoUpdater; - } else { - return; - } - - if (this.environmentService.disableUpdates) { - return; - } - - const channel = this.getUpdateChannel(); - const feedUrl = this.getUpdateFeedUrl(channel); - - if (!feedUrl) { - return; // updates not available - } - - try { - this.raw.setFeedURL(feedUrl); - } catch (e) { - return; // application not signed - } - - this.updateState(State.Idle); - - // Start checking for updates after 30 seconds - this.scheduleCheckForUpdates(30 * 1000) - .done(null, err => this.logService.error(err)); - } - - private scheduleCheckForUpdates(delay = 60 * 60 * 1000): TPromise { - return TPromise.timeout(delay) - .then(() => this.checkForUpdates()) - .then(update => { - if (update) { - // Update found, no need to check more - return TPromise.as(null); - } - - // Check again after 1 hour - return this.scheduleCheckForUpdates(60 * 60 * 1000); - }); - } - - checkForUpdates(explicit = false): TPromise { - return this.throttler.queue(() => this._checkForUpdates(explicit)) - .then(null, err => { - if (explicit) { - this._onError.fire(err); - } - - return null; - }); - } - - private _checkForUpdates(explicit: boolean): TPromise { - if (this.state !== State.Idle) { - return TPromise.as(null); - } - - this._onCheckForUpdate.fire(); - this.updateState(State.CheckingForUpdate); - - const listeners: IDisposable[] = []; - const result = new TPromise((c, e) => { - once(this.onRawError)(e, null, listeners); - once(this.onRawUpdateNotAvailable)(() => c(null), null, listeners); - once(this.onRawUpdateAvailable)(({ url, version }) => url && c({ url, version }), null, listeners); - once(this.onRawUpdateDownloaded)(({ version, date, releaseNotes, supportsFastUpdate }) => c({ version, date, releaseNotes, supportsFastUpdate }), null, listeners); - - this.raw.checkForUpdates(); - }).then(update => { - if (!update) { - this._onUpdateNotAvailable.fire(explicit); - this.updateState(State.Idle); - /* __GDPR__ - "update:notAvailable" : { - "explicit" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - } - */ - this.telemetryService.publicLog('update:notAvailable', { explicit }); - - // LINUX - } else if (update.url) { - const data: IUpdate = { - url: update.url, - releaseNotes: '', - version: update.version, - date: new Date() - }; - - this._availableUpdate = data; - this._onUpdateAvailable.fire({ url: update.url, version: update.version }); - this.updateState(State.UpdateAvailable); - /* __GDPR__ - "update:available" : { - "explicit" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "version": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "currentVersion": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - } - */ - this.telemetryService.publicLog('update:available', { explicit, version: update.version, currentVersion: product.commit }); - - } else { - const data: IRawUpdate = { - releaseNotes: update.releaseNotes, - version: update.version, - date: update.date, - supportsFastUpdate: update.supportsFastUpdate - }; - - this._availableUpdate = data; - - if (update.supportsFastUpdate) { - this._onUpdateDownloaded.fire(data); - this.updateState(State.UpdateDownloaded); - } else { - this._onUpdateReady.fire(data); - this.updateState(State.UpdateReady); - } - - /* __GDPR__ - "update:downloaded" : { - "version" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - } - */ - this.telemetryService.publicLog('update:downloaded', { version: update.version }); - } - - return update; - }, err => { - this.updateState(State.Idle); - return TPromise.wrapError(err); - }); - - return always(result, () => dispose(listeners)); - } - - private getUpdateChannel(): string { - const channel = this.configurationService.getValue('update.channel'); - return channel === 'none' ? null : product.quality; - } - - private getUpdateFeedUrl(channel: string): string { - if (!channel) { - return null; - } - - if (process.platform === 'win32' && !fs.existsSync(path.join(path.dirname(process.execPath), 'unins000.exe'))) { - return null; - } - - if (!product.updateUrl || !product.commit) { - return null; - } - - const platform = this.getUpdatePlatform(); - - return `${product.updateUrl}/api/update/${platform}/${channel}/${product.commit}`; - } - - private getUpdatePlatform(): string { - if (process.platform === 'linux') { - return `linux-${process.arch}`; - } - - if (process.platform === 'win32' && process.arch === 'x64') { - return 'win32-x64'; - } - - return process.platform; - } - - // for windows fast updates - applyUpdate(): TPromise { - if (this.state !== State.UpdateDownloaded) { - return TPromise.as(null); - } - - if (!this.raw.applyUpdate) { - return TPromise.as(null); - } - - once(this.onRawUpdateReady)(() => { - this._onUpdateReady.fire(this._availableUpdate as IRawUpdate); - this.updateState(State.UpdateReady); - }); - - once(this.onRawUpdateNotAvailable)(() => { - this._onUpdateNotAvailable.fire(false); - this.updateState(State.Idle); - }); - - this._onUpdateInstalling.fire(this._availableUpdate as IRawUpdate); - this.updateState(State.UpdateInstalling); - return this.raw.applyUpdate(); - } - - quitAndInstall(): TPromise { - if (!this._availableUpdate) { - return TPromise.as(null); - } - - if (this._availableUpdate.url) { - electron.shell.openExternal(this._availableUpdate.url); - return TPromise.as(null); - } - - this.logService.trace('update#quitAndInstall(): before lifecycle quit()'); - - this.lifecycleService.quit(true /* from update */).done(vetod => { - this.logService.trace(`update#quitAndInstall(): after lifecycle quit() with veto: ${vetod}`); - if (vetod) { - return; - } - - // for some reason updating on Mac causes the local storage not to be flushed. - // we workaround this issue by forcing an explicit flush of the storage data. - // see also https://github.com/Microsoft/vscode/issues/172 - if (process.platform === 'darwin') { - this.logService.trace('update#quitAndInstall(): calling flushStorageData()'); - electron.session.defaultSession.flushStorageData(); - } - - this.logService.trace('update#quitAndInstall(): running raw#quitAndInstall()'); - this.raw.quitAndInstall(); - }); - - return TPromise.as(null); - } -} diff --git a/src/vs/platform/update/electron-main/updateService.win32.ts b/src/vs/platform/update/electron-main/updateService.win32.ts new file mode 100644 index 00000000000..80562db75bd --- /dev/null +++ b/src/vs/platform/update/electron-main/updateService.win32.ts @@ -0,0 +1,207 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as fs from 'original-fs'; +import * as path from 'path'; +import * as pfs from 'vs/base/node/pfs'; +import { memoize } from 'vs/base/common/decorators'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ILifecycleService } from 'vs/platform/lifecycle/electron-main/lifecycleMain'; +import { IRequestService } from 'vs/platform/request/node/request'; +import product from 'vs/platform/node/product'; +import { TPromise, Promise } from 'vs/base/common/winjs.base'; +import { State, IUpdate, StateType } from 'vs/platform/update/common/update'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { ILogService } from 'vs/platform/log/common/log'; +import { createUpdateURL, AbstractUpdateService } from 'vs/platform/update/electron-main/abstractUpdateService'; +import { download, asJson } from 'vs/base/node/request'; +import { checksum } from 'vs/base/node/crypto'; +import { tmpdir } from 'os'; +import { spawn } from 'child_process'; + +function pollUntil(fn: () => boolean, timeout = 1000): TPromise { + return new TPromise(c => { + const poll = () => { + if (fn()) { + c(null); + } else { + setTimeout(poll, timeout); + } + }; + + poll(); + }); +} + +interface IAvailableUpdate { + packagePath: string; + updateFilePath?: string; +} + +export class Win32UpdateService extends AbstractUpdateService { + + _serviceBrand: any; + + private url: string | undefined; + private availableUpdate: IAvailableUpdate | undefined; + + @memoize + get cachePath(): TPromise { + const result = path.join(tmpdir(), `vscode-update-${process.arch}`); + return pfs.mkdirp(result, null).then(() => result); + } + + constructor( + @ILifecycleService lifecycleService: ILifecycleService, + @IConfigurationService configurationService: IConfigurationService, + @ITelemetryService private telemetryService: ITelemetryService, + @IEnvironmentService environmentService: IEnvironmentService, + @IRequestService private requestService: IRequestService, + @ILogService logService: ILogService + ) { + super(lifecycleService, configurationService, environmentService, logService); + } + + protected setUpdateFeedUrl(quality: string): boolean { + if (!fs.existsSync(path.join(path.dirname(process.execPath), 'unins000.exe'))) { + return false; + } + + this.url = createUpdateURL(process.arch === 'x64' ? 'win32-x64' : 'win32', quality); + return true; + } + + protected doCheckForUpdates(explicit: boolean): void { + if (!this.url) { + return; + } + + this.setState(State.CheckingForUpdates(explicit)); + + this.requestService.request({ url: this.url }) + .then(asJson) + .then(update => { + if (!update || !update.url || !update.version) { + /* __GDPR__ + "update:notAvailable" : { + "explicit" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } + */ + this.telemetryService.publicLog('update:notAvailable', { explicit }); + + this.setState(State.Idle); + return TPromise.as(null); + } + + this.setState(State.Downloading(update)); + + return this.cleanup(update.version).then(() => { + return this.getUpdatePackagePath(update.version).then(updatePackagePath => { + return pfs.exists(updatePackagePath).then(exists => { + if (exists) { + return TPromise.as(updatePackagePath); + } + + const url = update.url; + const hash = update.hash; + const downloadPath = `${updatePackagePath}.tmp`; + + return this.requestService.request({ url }) + .then(context => download(downloadPath, context)) + .then(hash ? () => checksum(downloadPath, update.hash) : () => null) + .then(() => pfs.rename(downloadPath, updatePackagePath)) + .then(() => updatePackagePath); + }); + }).then(packagePath => { + this.availableUpdate = { packagePath }; + + if (update.supportsFastUpdate) { + this.setState(State.Downloaded(update)); + } else { + this.setState(State.Ready(update)); + } + }); + }); + }) + .then(null, err => { + this.logService.error(err); + /* __GDPR__ + "update:notAvailable" : { + "explicit" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } + */ + this.telemetryService.publicLog('update:notAvailable', { explicit }); + this.setState(State.Idle); + }); + } + + private getUpdatePackagePath(version: string): TPromise { + return this.cachePath.then(cachePath => path.join(cachePath, `CodeSetup-${product.quality}-${version}.exe`)); + } + + private cleanup(exceptVersion: string = null): Promise { + const filter = exceptVersion ? one => !(new RegExp(`${product.quality}-${exceptVersion}\\.exe$`).test(one)) : () => true; + + return this.cachePath + .then(cachePath => pfs.readdir(cachePath) + .then(all => Promise.join(all + .filter(filter) + .map(one => pfs.unlink(path.join(cachePath, one)).then(null, () => null)) + )) + ); + } + + protected doApplyUpdate(): TPromise { + if (this.state.type !== StateType.Downloaded || !this.availableUpdate) { + return TPromise.as(null); + } + + const update = this.state.update; + this.setState(State.Updating(update)); + + return this.cachePath.then(cachePath => { + this.availableUpdate.updateFilePath = path.join(cachePath, `CodeSetup-${product.quality}-${update.version}.flag`); + + return pfs.writeFile(this.availableUpdate.updateFilePath, 'flag').then(() => { + const child = spawn(this.availableUpdate.packagePath, ['/verysilent', `/update="${this.availableUpdate.updateFilePath}"`, '/nocloseapplications', '/mergetasks=runcode,!desktopicon,!quicklaunchicon'], { + detached: true, + stdio: ['ignore', 'ignore', 'ignore'] + }); + + child.once('exit', () => { + this.availableUpdate = undefined; + this.setState(State.Idle); + }); + + const readyMutexName = `${product.win32MutexName}-ready`; + const isActive = (require.__$__nodeRequire('windows-mutex') as any).isActive; + + // poll for mutex-ready + pollUntil(() => isActive(readyMutexName)) + .then(() => this.setState(State.Ready(update))); + }); + }); + } + + protected doQuitAndInstall(): void { + if (this.state.type !== StateType.Ready) { + return; + } + + this.logService.trace('update#quitAndInstall(): running raw#quitAndInstall()'); + + if (this.state.update.supportsFastUpdate && this.availableUpdate.updateFilePath) { + fs.unlinkSync(this.availableUpdate.updateFilePath); + } else { + spawn(this.availableUpdate.packagePath, ['/silent', '/mergetasks=runcode,!desktopicon,!quicklaunchicon'], { + detached: true, + stdio: ['ignore', 'ignore', 'ignore'] + }); + } + } +} diff --git a/src/vs/workbench/parts/update/electron-browser/update.ts b/src/vs/workbench/parts/update/electron-browser/update.ts index 34e36f495bb..edb19e14670 100644 --- a/src/vs/workbench/parts/update/electron-browser/update.ts +++ b/src/vs/workbench/parts/update/electron-browser/update.ts @@ -9,7 +9,6 @@ import nls = require('vs/nls'); import severity from 'vs/base/common/severity'; import { TPromise } from 'vs/base/common/winjs.base'; import { IAction, Action } from 'vs/base/common/actions'; -import { mapEvent, filterEvent, once } from 'vs/base/common/event'; import { IDisposable, dispose, empty as EmptyDisposable } from 'vs/base/common/lifecycle'; import { Separator } from 'vs/base/browser/ui/actionbar/actionbar'; import { IMessageService, CloseAction, Severity } from 'vs/platform/message/common/message'; @@ -17,7 +16,7 @@ import pkg from 'vs/platform/node/package'; import product from 'vs/platform/node/product'; import URI from 'vs/base/common/uri'; import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { IActivityService, NumberBadge } from 'vs/workbench/services/activity/common/activity'; +import { IActivityService, NumberBadge, IBadge, ProgressBadge } from 'vs/workbench/services/activity/common/activity'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { ReleaseNotesInput } from 'vs/workbench/parts/update/electron-browser/releaseNotesInput'; import { IGlobalActivity } from 'vs/workbench/common/activity'; @@ -29,10 +28,11 @@ import { IOpenerService } from 'vs/platform/opener/common/opener'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; -import { IUpdateService, State as UpdateState } from 'vs/platform/update/common/update'; +import { IUpdateService, State as UpdateState, StateType, IUpdate } from 'vs/platform/update/common/update'; import * as semver from 'semver'; -import { OS, isLinux, isWindows } from 'vs/base/common/platform'; +import { OS } from 'vs/base/common/platform'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IWindowsService } from 'vs/platform/windows/common/windows'; const NotNowAction = new Action( 'update.later', @@ -282,6 +282,21 @@ class CommandAction extends Action { } } +export class DownloadNowAction extends Action { + + constructor( + private url: string, + @IWindowsService private windowsService: IWindowsService + ) { + super('update.downloadNow', nls.localize('download now', "Download Now"), null, true); + } + + run(): TPromise { + this.windowsService.openExternal(this.url); + return TPromise.as(null); + } +} + export class UpdateContribution implements IGlobalActivity { private static readonly showCommandsId = 'workbench.action.showCommands'; @@ -295,6 +310,7 @@ export class UpdateContribution implements IGlobalActivity { get name() { return ''; } get cssClass() { return 'update-activity'; } + private state: UpdateState; private badgeDisposable: IDisposable = EmptyDisposable; private disposables: IDisposable[] = []; @@ -307,27 +323,7 @@ export class UpdateContribution implements IGlobalActivity { @IWorkbenchEditorService editorService: IWorkbenchEditorService, @IActivityService private activityService: IActivityService ) { - if (isLinux) { - mapEvent(updateService.onUpdateAvailable, e => e.version) - (this.onUpdateAvailable, this, this.disposables); - } else if (isWindows) { - // fast updates - mapEvent(updateService.onUpdateDownloaded, e => e.version) - (this.onUpdateDownloaded, this, this.disposables); - mapEvent(updateService.onUpdateInstalling, e => e.version) - (this.onUpdateInstalling, this, this.disposables); - - // regular old updates - mapEvent(filterEvent(updateService.onUpdateReady, e => !e.supportsFastUpdate), e => e.version) - (this.onUpdateAvailable, this, this.disposables); - - } else { - mapEvent(updateService.onUpdateReady, e => e.version) - (this.onUpdateAvailable, this, this.disposables); - } - - updateService.onError(this.onError, this, this.disposables); - updateService.onUpdateNotAvailable(this.onUpdateNotAvailable, this, this.disposables); + this.state = updateService.state; updateService.onStateChange(this.onUpdateStateChange, this, this.disposables); this.onUpdateStateChange(this.updateService.state); @@ -351,16 +347,110 @@ export class UpdateContribution implements IGlobalActivity { } private onUpdateStateChange(state: UpdateState): void { + switch (state.type) { + case StateType.Idle: + if (this.state.type === StateType.CheckingForUpdates && this.state.explicit) { + this.onUpdateNotAvailable(); + } + break; + + case StateType.Available: + this.onUpdateAvailable(state.update); + break; + + case StateType.Downloaded: + this.onUpdateDownloaded(state.update); + break; + + case StateType.Updating: + this.onUpdateUpdating(state.update); + break; + + case StateType.Ready: + this.onUpdateReady(state.update); + break; + } + + let badge: IBadge | undefined = undefined; + + if (state.type === StateType.Available || state.type === StateType.Downloaded || state.type === StateType.Ready) { + badge = new NumberBadge(1, () => nls.localize('updateIsReady', "New {0} update available.", product.nameShort)); + } else if (state.type === StateType.CheckingForUpdates || state.type === StateType.Downloading || state.type === StateType.Updating) { + badge = new ProgressBadge(() => nls.localize('updateIsReady', "New {0} update available.", product.nameShort)); + } + this.badgeDisposable.dispose(); - const isUpdateAvailable = isLinux - ? state === UpdateState.UpdateAvailable - : state === UpdateState.UpdateDownloaded || state === UpdateState.UpdateReady; - - if (isUpdateAvailable) { - const badge = new NumberBadge(1, () => nls.localize('updateIsReady', "New {0} update available.", product.nameShort)); + if (badge) { this.badgeDisposable = this.activityService.showActivity(this.id, badge); } + + this.state = state; + } + + private onUpdateNotAvailable(): void { + this.messageService.show(severity.Info, nls.localize('noUpdatesAvailable', "There are no updates currently available.")); + } + + // linux + private onUpdateAvailable(update: IUpdate): void { + if (!this.shouldShowNotification()) { + return; + } + + const releaseNotesAction = this.instantiationService.createInstance(ShowReleaseNotesAction, update.productVersion); + const downloadAction = this.instantiationService.createInstance(DownloadNowAction, update.url); + + this.messageService.show(severity.Info, { + message: nls.localize('thereIsUpdateAvailable', "There is an available update."), + actions: [downloadAction, NotNowAction, releaseNotesAction] + }); + } + + // windows fast updates + private onUpdateDownloaded(update: IUpdate): void { + if (!this.shouldShowNotification()) { + return; + } + + const releaseNotesAction = this.instantiationService.createInstance(ShowReleaseNotesAction, update.productVersion); + const installUpdateAction = new Action('update.applyUpdate', nls.localize('installUpdate', "Install Update"), undefined, true, () => + this.updateService.applyUpdate()); + + this.messageService.show(severity.Info, { + message: nls.localize('updateAvailable', "There's an available update: {0} {1}", product.nameLong, update.productVersion), + actions: [installUpdateAction, NotNowAction, releaseNotesAction] + }); + } + + // windows fast updates + private onUpdateUpdating(update: IUpdate): void { + const neverShowAgain = new NeverShowAgain('update/win32-fast-updates', this.storageService); + + if (!neverShowAgain.shouldShow()) { + return; + } + + this.messageService.show(severity.Info, { + message: nls.localize('updateInstalling', "{0} {1} is being installed in the background, we'll let you know when it's done.", product.nameLong, update.productVersion), + actions: [CloseAction, neverShowAgain.action] + }); + } + + // windows and mac + private onUpdateReady(update: IUpdate): void { + if (!this.shouldShowNotification()) { + return; + } + + const releaseNotesAction = this.instantiationService.createInstance(ShowReleaseNotesAction, update.productVersion); + const applyUpdateAction = new Action('update.applyUpdate', nls.localize('updateNow', "Update Now"), undefined, true, () => + this.updateService.quitAndInstall()); + + this.messageService.show(severity.Info, { + message: nls.localize('updateAvailableAfterRestart', "{0} will be updated after it restarts.", product.nameLong), + actions: [applyUpdateAction, NotNowAction, releaseNotesAction] + }); } private shouldShowNotification(): boolean { @@ -380,91 +470,8 @@ export class UpdateContribution implements IGlobalActivity { return diffDays > 5; } - // windows fast updates - private onUpdateDownloaded(version: string): void { - if (!this.shouldShowNotification()) { - return; - } - - const releaseNotesAction = this.instantiationService.createInstance(ShowReleaseNotesAction, version); - const installUpdateAction = new Action('update.applyUpdate', nls.localize('installUpdate', "Install Update"), undefined, true, () => { - once(mapEvent(filterEvent(this.updateService.onUpdateReady, e => e.supportsFastUpdate), e => e.version)) - (this.onWindowsFastUpdateReady, this); - - return this.updateService.applyUpdate(); - }); - - this.messageService.show(severity.Info, { - message: nls.localize('updateAvailable', "There's an available update: {0} {1}", product.nameLong, version), - actions: [installUpdateAction, NotNowAction, releaseNotesAction] - }); - } - - // windows fast updates - private onWindowsFastUpdateReady(version: string): void { - const releaseNotesAction = this.instantiationService.createInstance(ShowReleaseNotesAction, version); - const restartAction = new Action('update.applyUpdate', nls.localize('updateNow', "Update Now"), undefined, true, () => this.updateService.quitAndInstall()); - - this.messageService.show(severity.Info, { - message: nls.localize('updateAvailableAfterRestart', "{0} will be updated after it restarts.", product.nameLong), - actions: [restartAction, NotNowAction, releaseNotesAction] - }); - } - - // windows fast updates - private onUpdateInstalling(version: string): void { - const neverShowAgain = new NeverShowAgain('update/win32-fast-updates', this.storageService); - - if (!neverShowAgain.shouldShow()) { - return; - } - - this.messageService.show(severity.Info, { - message: nls.localize('updateInstalling', "{0} {1} is being installed in the background, we'll let you know when it's done.", product.nameLong, version), - actions: [CloseAction, neverShowAgain.action] - }); - } - - private onUpdateAvailable(version: string): void { - if (!this.shouldShowNotification()) { - return; - } - - const releaseNotesAction = this.instantiationService.createInstance(ShowReleaseNotesAction, version); - - if (isLinux) { - const downloadAction = new Action('update.download', nls.localize('downloadNow', "Download Now"), undefined, true, () => this.updateService.quitAndInstall()); - - this.messageService.show(severity.Info, { - message: nls.localize('thereIsUpdateAvailable', "There is an available update."), - actions: [downloadAction, NotNowAction, releaseNotesAction] - }); - } else { - const applyUpdateAction = new Action('update.applyUpdate', nls.localize('updateNow', "Update Now"), undefined, true, () => this.updateService.quitAndInstall()); - - this.messageService.show(severity.Info, { - message: nls.localize('updateAvailableAfterRestart', "{0} will be updated after it restarts.", product.nameLong), - actions: [applyUpdateAction, NotNowAction, releaseNotesAction] - }); - } - } - - private onUpdateNotAvailable(explicit: boolean): void { - if (!explicit) { - return; - } - - this.messageService.show(severity.Info, nls.localize('noUpdatesAvailable', "There are no updates currently available.")); - } - - private onError(err: any): void { - this.messageService.show(severity.Error, err); - } - getActions(): IAction[] { - const updateAction = this.getUpdateAction(); - - return [ + const result: IAction[] = [ new CommandAction(UpdateContribution.showCommandsId, nls.localize('commandPalette', "Command Palette..."), this.commandService), new Separator(), new CommandAction(UpdateContribution.openSettingsId, nls.localize('settings', "Settings"), this.commandService), @@ -473,46 +480,48 @@ export class UpdateContribution implements IGlobalActivity { new CommandAction(UpdateContribution.openUserSnippets, nls.localize('userSnippets', "User Snippets"), this.commandService), new Separator(), new CommandAction(UpdateContribution.selectColorThemeId, nls.localize('selectTheme.label', "Color Theme"), this.commandService), - new CommandAction(UpdateContribution.selectIconThemeId, nls.localize('themes.selectIconTheme.label', "File Icon Theme"), this.commandService), - new Separator(), - updateAction + new CommandAction(UpdateContribution.selectIconThemeId, nls.localize('themes.selectIconTheme.label', "File Icon Theme"), this.commandService) ]; + + const updateAction = this.getUpdateAction(); + + if (updateAction) { + result.push(new Separator(), updateAction); + } + + return result; } - private getUpdateAction(): IAction { - switch (this.updateService.state) { - case UpdateState.Uninitialized: - return new Action('update.notavailable', nls.localize('not available', "Updates Not Available"), undefined, false); + private getUpdateAction(): IAction | null { + const state = this.updateService.state; - case UpdateState.CheckingForUpdate: + switch (state.type) { + case StateType.Uninitialized: + return null; + + case StateType.Idle: + return new Action('update.check', nls.localize('checkForUpdates', "Check for Updates..."), undefined, true, () => + this.updateService.checkForUpdates(true)); + + case StateType.CheckingForUpdates: return new Action('update.checking', nls.localize('checkingForUpdates', "Checking For Updates..."), undefined, false); - case UpdateState.UpdateAvailable: - if (isLinux) { - return new Action('update.linux.available', nls.localize('DownloadUpdate', "Download Available Update"), undefined, true, () => - this.updateService.quitAndInstall()); - } + case StateType.Available: + return this.instantiationService.createInstance(DownloadNowAction, state.update.url); - const updateAvailableLabel = isWindows - ? nls.localize('DownloadingUpdate', "Downloading Update...") - : nls.localize('InstallingUpdate', "Installing Update..."); + case StateType.Downloading: + return new Action('update.downloading', nls.localize('DownloadingUpdate', "Downloading Update..."), undefined, false); - return new Action('update.available', updateAvailableLabel, undefined, false); - - case UpdateState.UpdateDownloaded: - return new Action('update.apply', nls.localize('installUpdate...', "Install Update..."), undefined, true, () => + case StateType.Downloaded: + return new Action('update.install', nls.localize('installUpdate...', "Install Update..."), undefined, true, () => this.updateService.applyUpdate()); - case UpdateState.UpdateInstalling: - return new Action('update.applying', nls.localize('installingUpdate', "Installing Update..."), undefined, false); + case StateType.Updating: + return new Action('update.updating', nls.localize('installingUpdate', "Installing Update..."), undefined, false); - case UpdateState.UpdateReady: + case StateType.Ready: return new Action('update.restart', nls.localize('restartToUpdate', "Restart to Update..."), undefined, true, () => this.updateService.quitAndInstall()); - - default: - return new Action('update.check', nls.localize('checkForUpdates', "Check for Updates..."), undefined, this.updateService.state === UpdateState.Idle, () => - this.updateService.checkForUpdates(true)); } } From ca91ad4ee4d3b074dfb8e7a090f00e6eb26b8b8e Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Fri, 19 Jan 2018 21:14:04 +0100 Subject: [PATCH 018/128] fix state check --- src/vs/platform/update/electron-main/abstractUpdateService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/platform/update/electron-main/abstractUpdateService.ts b/src/vs/platform/update/electron-main/abstractUpdateService.ts index f7fa618119c..fcbb2303851 100644 --- a/src/vs/platform/update/electron-main/abstractUpdateService.ts +++ b/src/vs/platform/update/electron-main/abstractUpdateService.ts @@ -89,7 +89,7 @@ export abstract class AbstractUpdateService implements IUpdateService { } checkForUpdates(explicit = false): TPromise { - if (this.state !== State.Idle) { + if (this.state.type !== StateType.Idle) { return TPromise.as(null); } From 0192c680eb8c43e31b0dde9e56c3ce3fdc151223 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Fri, 19 Jan 2018 22:13:21 +0100 Subject: [PATCH 019/128] linux: state should be idle after download --- src/vs/code/electron-main/menus.ts | 2 +- src/vs/platform/update/common/update.ts | 1 + src/vs/platform/update/common/updateIpc.ts | 6 +++++ .../electron-main/abstractUpdateService.ts | 14 +++++++++++- .../electron-main/updateService.linux.ts | 11 +++++++--- .../parts/update/electron-browser/update.ts | 22 ++++--------------- 6 files changed, 33 insertions(+), 23 deletions(-) diff --git a/src/vs/code/electron-main/menus.ts b/src/vs/code/electron-main/menus.ts index 7d419505b84..5c1daf2bea3 100644 --- a/src/vs/code/electron-main/menus.ts +++ b/src/vs/code/electron-main/menus.ts @@ -1060,7 +1060,7 @@ export class CodeMenu { case StateType.Available: return [new MenuItem({ label: nls.localize('miDownloadUpdate', "Download Available Update"), click: () => { - shell.openExternal(state.update.url); + this.updateService.downloadUpdate(); } })]; diff --git a/src/vs/platform/update/common/update.ts b/src/vs/platform/update/common/update.ts index 5a397113d90..a80618c9ebb 100644 --- a/src/vs/platform/update/common/update.ts +++ b/src/vs/platform/update/common/update.ts @@ -68,6 +68,7 @@ export interface IUpdateService { readonly state: State; checkForUpdates(explicit: boolean): TPromise; + downloadUpdate(): TPromise; applyUpdate(): TPromise; quitAndInstall(): TPromise; } \ No newline at end of file diff --git a/src/vs/platform/update/common/updateIpc.ts b/src/vs/platform/update/common/updateIpc.ts index 9aef4457e9b..441bdd138e1 100644 --- a/src/vs/platform/update/common/updateIpc.ts +++ b/src/vs/platform/update/common/updateIpc.ts @@ -13,6 +13,7 @@ import { IUpdateService, State } from './update'; export interface IUpdateChannel extends IChannel { call(command: 'checkForUpdates', arg: boolean): TPromise; + call(command: 'downloadUpdate'): TPromise; call(command: 'applyUpdate'): TPromise; call(command: 'quitAndInstall'): TPromise; call(command: '_getInitialState'): TPromise; @@ -27,6 +28,7 @@ export class UpdateChannel implements IUpdateChannel { switch (command) { case 'event:onStateChange': return eventToCall(this.service.onStateChange); case 'checkForUpdates': return this.service.checkForUpdates(arg); + case 'downloadUpdate': return this.service.downloadUpdate(); case 'applyUpdate': return this.service.applyUpdate(); case 'quitAndInstall': return this.service.quitAndInstall(); case '_getInitialState': return TPromise.as(this.service.state); @@ -64,6 +66,10 @@ export class UpdateChannelClient implements IUpdateService { return this.channel.call('checkForUpdates', explicit); } + downloadUpdate(): TPromise { + return this.channel.call('downloadUpdate'); + } + applyUpdate(): TPromise { return this.channel.call('applyUpdate'); } diff --git a/src/vs/platform/update/electron-main/abstractUpdateService.ts b/src/vs/platform/update/electron-main/abstractUpdateService.ts index fcbb2303851..742d09737d1 100644 --- a/src/vs/platform/update/electron-main/abstractUpdateService.ts +++ b/src/vs/platform/update/electron-main/abstractUpdateService.ts @@ -11,7 +11,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { ILifecycleService } from 'vs/platform/lifecycle/electron-main/lifecycleMain'; import product from 'vs/platform/node/product'; import { TPromise } from 'vs/base/common/winjs.base'; -import { IUpdateService, State, StateType } from 'vs/platform/update/common/update'; +import { IUpdateService, State, StateType, Available } from 'vs/platform/update/common/update'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { ILogService } from 'vs/platform/log/common/log'; @@ -96,6 +96,18 @@ export abstract class AbstractUpdateService implements IUpdateService { return this.throttler.queue(() => TPromise.as(this.doCheckForUpdates(explicit))); } + downloadUpdate(): TPromise { + if (this.state.type !== StateType.Available) { + return TPromise.as(null); + } + + return this.doDownloadUpdate(this.state); + } + + protected doDownloadUpdate(state: Available): TPromise { + return TPromise.as(null); + } + applyUpdate(): TPromise { if (this.state.type !== StateType.Ready) { return TPromise.as(null); diff --git a/src/vs/platform/update/electron-main/updateService.linux.ts b/src/vs/platform/update/electron-main/updateService.linux.ts index a8c1ea132b2..a59e24fd722 100644 --- a/src/vs/platform/update/electron-main/updateService.linux.ts +++ b/src/vs/platform/update/electron-main/updateService.linux.ts @@ -8,12 +8,14 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ILifecycleService } from 'vs/platform/lifecycle/electron-main/lifecycleMain'; import { IRequestService } from 'vs/platform/request/node/request'; -import { State, IUpdate } from 'vs/platform/update/common/update'; +import { State, IUpdate, Available } from 'vs/platform/update/common/update'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { ILogService } from 'vs/platform/log/common/log'; import { createUpdateURL, AbstractUpdateService } from 'vs/platform/update/electron-main/abstractUpdateService'; import { asJson } from 'vs/base/node/request'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { shell } from 'electron'; export class LinuxUpdateService extends AbstractUpdateService { @@ -71,7 +73,10 @@ export class LinuxUpdateService extends AbstractUpdateService { }); } - protected doQuitAndInstall(): void { - // not available + protected doDownloadUpdate(state: Available): TPromise { + shell.openExternal(state.update.url); + this.setState(State.Idle); + + return TPromise.as(null); } } diff --git a/src/vs/workbench/parts/update/electron-browser/update.ts b/src/vs/workbench/parts/update/electron-browser/update.ts index 87fcca32d3c..e091ce43563 100644 --- a/src/vs/workbench/parts/update/electron-browser/update.ts +++ b/src/vs/workbench/parts/update/electron-browser/update.ts @@ -32,7 +32,6 @@ import { IUpdateService, State as UpdateState, StateType, IUpdate } from 'vs/pla import * as semver from 'semver'; import { OS } from 'vs/base/common/platform'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { IWindowsService } from 'vs/platform/windows/common/windows'; const NotNowAction = new Action( 'update.later', @@ -282,21 +281,6 @@ class CommandAction extends Action { } } -export class DownloadNowAction extends Action { - - constructor( - private url: string, - @IWindowsService private windowsService: IWindowsService - ) { - super('update.downloadNow', nls.localize('download now', "Download Now"), null, true); - } - - run(): TPromise { - this.windowsService.openExternal(this.url); - return TPromise.as(null); - } -} - export class UpdateContribution implements IGlobalActivity { private static readonly showCommandsId = 'workbench.action.showCommands'; @@ -399,7 +383,8 @@ export class UpdateContribution implements IGlobalActivity { } const releaseNotesAction = this.instantiationService.createInstance(ShowReleaseNotesAction, update.productVersion); - const downloadAction = this.instantiationService.createInstance(DownloadNowAction, update.url); + const downloadAction = new Action('update.downloadNow', nls.localize('download now', "Download Now"), null, true, () => + this.updateService.downloadUpdate()); this.messageService.show(severity.Info, { message: nls.localize('thereIsUpdateAvailable', "There is an available update."), @@ -507,7 +492,8 @@ export class UpdateContribution implements IGlobalActivity { return new Action('update.checking', nls.localize('checkingForUpdates', "Checking For Updates..."), undefined, false); case StateType.Available: - return this.instantiationService.createInstance(DownloadNowAction, state.update.url); + return new Action('update.downloadNow', nls.localize('download now', "Download Now"), null, true, () => + this.updateService.downloadUpdate()); case StateType.Downloading: return new Action('update.downloading', nls.localize('DownloadingUpdate', "Downloading Update..."), undefined, false); From 50064a2501768cd94b39598f83206f25c1d36892 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Fri, 19 Jan 2018 22:35:27 +0100 Subject: [PATCH 020/128] enableWindowsBackgroundUpdates --- .../platform/update/electron-main/abstractUpdateService.ts | 2 +- src/vs/platform/update/electron-main/updateService.win32.ts | 4 +++- .../parts/update/electron-browser/update.contribution.ts | 5 +++++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/vs/platform/update/electron-main/abstractUpdateService.ts b/src/vs/platform/update/electron-main/abstractUpdateService.ts index 742d09737d1..ade88f2e6f5 100644 --- a/src/vs/platform/update/electron-main/abstractUpdateService.ts +++ b/src/vs/platform/update/electron-main/abstractUpdateService.ts @@ -40,7 +40,7 @@ export abstract class AbstractUpdateService implements IUpdateService { constructor( @ILifecycleService private lifecycleService: ILifecycleService, - @IConfigurationService private configurationService: IConfigurationService, + @IConfigurationService protected configurationService: IConfigurationService, @IEnvironmentService private environmentService: IEnvironmentService, @ILogService protected logService: ILogService ) { diff --git a/src/vs/platform/update/electron-main/updateService.win32.ts b/src/vs/platform/update/electron-main/updateService.win32.ts index 80562db75bd..b50b3ac33b6 100644 --- a/src/vs/platform/update/electron-main/updateService.win32.ts +++ b/src/vs/platform/update/electron-main/updateService.win32.ts @@ -118,9 +118,11 @@ export class Win32UpdateService extends AbstractUpdateService { .then(() => updatePackagePath); }); }).then(packagePath => { + const fastUpdatesEnabled = this.configurationService.getValue('update.enableWindowsBackgroundUpdates'); + this.availableUpdate = { packagePath }; - if (update.supportsFastUpdate) { + if (fastUpdatesEnabled && update.supportsFastUpdate) { this.setState(State.Downloaded(update)); } else { this.setState(State.Ready(update)); diff --git a/src/vs/workbench/parts/update/electron-browser/update.contribution.ts b/src/vs/workbench/parts/update/electron-browser/update.contribution.ts index d93239ac3ca..b533e60a755 100644 --- a/src/vs/workbench/parts/update/electron-browser/update.contribution.ts +++ b/src/vs/workbench/parts/update/electron-browser/update.contribution.ts @@ -57,6 +57,11 @@ configurationRegistry.registerConfiguration({ 'enum': ['none', 'default'], 'default': 'default', 'description': nls.localize('updateChannel', "Configure whether you receive automatic updates from an update channel. Requires a restart after change.") + }, + 'update.enableWindowsBackgroundUpdates': { + 'type': 'boolean', + 'default': false, + 'description': nls.localize('enableWindowsBackgroundUpdates', "Enables Windows background updates.") } } }); From 04d38863910098da9224eb3a6d5e9a3ca531192c Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Fri, 19 Jan 2018 22:47:12 +0100 Subject: [PATCH 021/128] win32: code should run after regular update --- build/win32/code.iss | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/build/win32/code.iss b/build/win32/code.iss index 7e8326e4313..fc9b79a33c8 100644 --- a/build/win32/code.iss +++ b/build/win32/code.iss @@ -961,30 +961,30 @@ begin end; // Updates -function IsUpdate(): Boolean; +function IsBackgroundUpdate(): Boolean; begin Result := ExpandConstant('{param:update|false}') <> 'false'; end; function IsNotUpdate(): Boolean; begin - Result := not IsUpdate(); + 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 ShouldRunAfterUpdate(): Boolean; begin - if IsUpdate() then + 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}')) else - Result := False; + Result := True; end; function GetAppMutex(Value: string): string; begin - if IsUpdate() then + if IsBackgroundUpdate() then Result := '' else Result := '{#AppMutex}'; @@ -992,7 +992,7 @@ end; function GetDestDir(Value: string): string; begin - if IsUpdate() then + if IsBackgroundUpdate() then Result := ExpandConstant('{app}\_') else Result := ExpandConstant('{app}'); @@ -1002,7 +1002,7 @@ procedure CurStepChanged(CurStep: TSetupStep); var UpdateResultCode: Integer; begin - if IsUpdate() and (CurStep = ssPostInstall) then + if IsBackgroundUpdate() and (CurStep = ssPostInstall) then begin CreateMutex('{#AppMutex}-ready'); From 91198d94747caad4c0041351054b1f2b52135d51 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Fri, 19 Jan 2018 15:20:57 -0800 Subject: [PATCH 022/128] Request settings for new extensions --- .../preferences/browser/preferencesEditor.ts | 72 ++++++++++------- .../parts/preferences/common/preferences.ts | 2 +- .../electron-browser/preferencesSearch.ts | 78 +++++++++++-------- 3 files changed, 90 insertions(+), 62 deletions(-) diff --git a/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts b/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts index 3d60b7d794f..d6e0b6c55cb 100644 --- a/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts +++ b/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts @@ -111,7 +111,7 @@ export class PreferencesEditor extends BaseEditor { private preferencesRenderers: PreferencesRenderersController; private delayedFilterLogging: Delayer; - private remoteSearchThrottle: ThrottledDelayer; + private remoteSearchThrottle: ThrottledDelayer; private _lastReportedFilter: string; private lastFocusedWidget: SearchWidget | SideBySidePreferencesWidget = null; @@ -243,18 +243,18 @@ export class PreferencesEditor extends BaseEditor { TPromise.join([ this.preferencesRenderers.localFilterPreferences(query), this.triggerThrottledSearch(query) - ]).then(results => { - if (results) { - const [localResult, remoteResult] = results; + ]).then(() => { + const result = this.preferencesRenderers.lastFilterResult; + if (result) { this.delayedFilterLogging.trigger(() => this.reportFilteringUsed( query, - remoteResult ? remoteResult.defaultSettingsGroupCounts : localResult.defaultSettingsGroupCounts, - remoteResult && remoteResult.metadata)); + result.defaultSettingsGroupCounts, + result.metadata)); } }); } - private triggerThrottledSearch(query: string): TPromise { + private triggerThrottledSearch(query: string): TPromise { if (query) { return this.remoteSearchThrottle.trigger(() => this.preferencesRenderers.remoteSearchPreferences(query)); } else { @@ -366,11 +366,13 @@ class PreferencesRenderersController extends Disposable { private _editablePreferencesRendererDisposables: IDisposable[] = []; private _settingsNavigator: SettingsNavigator; - private _filtersInProgress: TPromise[]; + private _remoteFiltersInProgress: TPromise[]; private _currentLocalSearchProvider: ISearchProvider; private _currentRemoteSearchProvider: ISearchProvider; + private _currentNewExtensionsSearchProvider: ISearchProvider; private _lastQuery: string; + private _lastFilterResult: IFilterOrSearchResult; private _onDidFilterResultsCountChange: Emitter = this._register(new Emitter()); public onDidFilterResultsCountChange: Event = this._onDidFilterResultsCountChange.event; @@ -382,6 +384,10 @@ class PreferencesRenderersController extends Disposable { super(); } + get lastFilterResult(): IFilterOrSearchResult { + return this._lastFilterResult; + } + get defaultPreferencesRenderer(): IPreferencesRenderer { return this._defaultPreferencesRenderer; } @@ -415,34 +421,48 @@ class PreferencesRenderersController extends Disposable { } } - async _onEditableContentDidChange(): TPromise { + private async _onEditableContentDidChange(): TPromise { await this.localFilterPreferences(this._lastQuery, true); await this.remoteSearchPreferences(this._lastQuery, true); } - remoteSearchPreferences(query: string, updateCurrentResults?: boolean): TPromise { + remoteSearchPreferences(query: string, updateCurrentResults?: boolean): TPromise { + if (this._remoteFiltersInProgress) { + // Resolved/rejected promises have no .cancel() + this._remoteFiltersInProgress.forEach(p => p.cancel && p.cancel()); + } + this._currentRemoteSearchProvider = (updateCurrentResults && this._currentRemoteSearchProvider) || this.preferencesSearchService.getRemoteSearchProvider(query); - return this.filterOrSearchPreferences(query, this._currentRemoteSearchProvider, 'nlpResult', nls.localize('nlpResult', "Natural Language Results")); + this._currentNewExtensionsSearchProvider = (updateCurrentResults && this._currentNewExtensionsSearchProvider) || this.preferencesSearchService.getRemoteSearchProvider(query, true); + + this._remoteFiltersInProgress = [ + this.filterOrSearchPreferences(query, this._currentNewExtensionsSearchProvider, 'newExtensionsResult', nls.localize('newExtensionsResult', "Other Extension Results")), + this.filterOrSearchPreferences(query, this._currentRemoteSearchProvider, 'nlpResult', nls.localize('nlpResult', "Natural Language Results")) + ]; + + return TPromise.join(this._remoteFiltersInProgress).then(() => { + this._remoteFiltersInProgress = null; + }, err => { + if (isPromiseCanceledError(err)) { + return null; + } else { + onUnexpectedError(err); + } + }); } - localFilterPreferences(query: string, updateCurrentResults?: boolean): TPromise { + localFilterPreferences(query: string, updateCurrentResults?: boolean): TPromise { this._currentLocalSearchProvider = (updateCurrentResults && this._currentLocalSearchProvider) || this.preferencesSearchService.getLocalSearchProvider(query); return this.filterOrSearchPreferences(query, this._currentLocalSearchProvider, 'filterResult', nls.localize('filterResult', "Filtered Results")); } - filterOrSearchPreferences(query: string, searchProvider: ISearchProvider, groupId: string, groupLabel: string): TPromise { + private filterOrSearchPreferences(query: string, searchProvider: ISearchProvider, groupId: string, groupLabel: string): TPromise { this._lastQuery = query; - if (this._filtersInProgress) { - // Resolved/rejected promises have no .cancel() - this._filtersInProgress.forEach(p => p.cancel && p.cancel()); - } - this._filtersInProgress = [ + return TPromise.join([ this._filterOrSearchPreferences(query, this.defaultPreferencesRenderer, searchProvider, groupId, groupLabel), - this._filterOrSearchPreferences(query, this.editablePreferencesRenderer, searchProvider, groupId, groupLabel)]; - - return TPromise.join(this._filtersInProgress).then(results => { - this._filtersInProgress = null; + this._filterOrSearchPreferences(query, this.editablePreferencesRenderer, searchProvider, groupId, groupLabel)] + ).then(results => { const [defaultFilterResult, editableFilterResult] = results; this.consolidateAndUpdate(defaultFilterResult, editableFilterResult); @@ -451,13 +471,7 @@ class PreferencesRenderersController extends Disposable { defaultSettingsGroupCounts: defaultFilterResult && this._countById(defaultFilterResult.filteredGroups) }; - return result; - }, err => { - if (isPromiseCanceledError(err)) { - return null; - } else { - onUnexpectedError(err); - } + this._lastFilterResult = result; }); } diff --git a/src/vs/workbench/parts/preferences/common/preferences.ts b/src/vs/workbench/parts/preferences/common/preferences.ts index 9330e27ba91..d4ba6f83a05 100644 --- a/src/vs/workbench/parts/preferences/common/preferences.ts +++ b/src/vs/workbench/parts/preferences/common/preferences.ts @@ -177,7 +177,7 @@ export interface IPreferencesSearchService { _serviceBrand: any; getLocalSearchProvider(filter: string): ISearchProvider; - getRemoteSearchProvider(filter: string): ISearchProvider; + getRemoteSearchProvider(filter: string, newExtensionsOnly?: boolean): ISearchProvider; } export interface ISearchProvider { diff --git a/src/vs/workbench/parts/preferences/electron-browser/preferencesSearch.ts b/src/vs/workbench/parts/preferences/electron-browser/preferencesSearch.ts index 763be7bcc05..3c56eac7ff6 100644 --- a/src/vs/workbench/parts/preferences/electron-browser/preferencesSearch.ts +++ b/src/vs/workbench/parts/preferences/electron-browser/preferencesSearch.ts @@ -67,8 +67,8 @@ export class PreferencesSearchService extends Disposable implements IPreferences } } - getRemoteSearchProvider(filter: string): RemoteSearchProvider { - return this.remoteSearchAllowed && this.instantiationService.createInstance(RemoteSearchProvider, filter, this._endpoint, this._installedExtensions); + getRemoteSearchProvider(filter: string, newExtensionsOnly = false): RemoteSearchProvider { + return this.remoteSearchAllowed && this.instantiationService.createInstance(RemoteSearchProvider, filter, this._endpoint, this._installedExtensions, newExtensionsOnly); } getLocalSearchProvider(filter: string): LocalSearchProvider { @@ -117,7 +117,7 @@ export class RemoteSearchProvider implements ISearchProvider { private _filter: string; private _remoteSearchP: TPromise; - constructor(filter: string, endpoint: IEndpointDetails, private installedExtensions: TPromise, + constructor(filter: string, private endpoint: IEndpointDetails, private installedExtensions: TPromise, private newExtensionsOnly: boolean, @IEnvironmentService private environmentService: IEnvironmentService, @IRequestService private requestService: IRequestService, ) { @@ -125,7 +125,7 @@ export class RemoteSearchProvider implements ISearchProvider { // @queries are always handled by local filter this._remoteSearchP = filter && !strings.startsWith(filter, '@') ? - this.getSettingsFromBing(filter, endpoint) : + this.getSettingsFromBing(filter) : TPromise.wrap(null); } @@ -148,15 +148,15 @@ export class RemoteSearchProvider implements ISearchProvider { }); } - private getSettingsFromBing(filter: string, endpoint: IEndpointDetails): TPromise { + private getSettingsFromBing(filter: string): TPromise { const start = Date.now(); - return this.prepareUrl(filter, endpoint, this.environmentService.settingsSearchBuildId).then(url => { + return this.prepareUrl(filter).then(url => { return this.requestService.request({ url, headers: { 'User-Agent': 'request', 'Content-Type': 'application/json; charset=utf-8', - 'api-key': endpoint.key + 'api-key': this.endpoint.key }, timeout: 5000 }).then(context => { @@ -205,7 +205,7 @@ export class RemoteSearchProvider implements ISearchProvider { }; } - private prepareUrl(query: string, endpoint: IEndpointDetails, buildNumber: number): TPromise { + private prepareUrl(query: string): TPromise { query = escapeSpecialChars(query); const boost = 10; const userQuery = `(${query})^${boost}`; @@ -214,41 +214,55 @@ export class RemoteSearchProvider implements ISearchProvider { query = query.replace(/\ +/g, '~ ') + '~'; const encodedQuery = encodeURIComponent(userQuery + ' || ' + query); - let url = `${endpoint.urlBase}?`; + let url = `${this.endpoint.urlBase}?`; - return this.installedExtensions.then(exts => { - if (endpoint.key) { - url += `${API_VERSION}`; - url += `&search=${encodedQuery}`; + const buildNumber = this.environmentService.settingsSearchBuildId; + if (this.endpoint.key) { + url += `${API_VERSION}&${QUERY_TYPE}`; + url += `&search=${encodedQuery}`; - const filters = exts.map(ext => { - const uuid = ext.identifier.uuid; - const versionString = ext.manifest.version - .split('.') - .map(versionPart => strings.pad(versionPart, 10)) - .join(''); - - return `(packageid eq '${uuid}' and startbuildno le '${versionString}' and endbuildno ge '${versionString}')`; - }); - - if (buildNumber) { - filters.push(`(packageid eq 'core' and startbuildno le '${buildNumber}' and endbuildno ge '${buildNumber}')`); - url += `&$filter=${filters.join(' or ')}`; - } + if (this.newExtensionsOnly) { + return TPromise.wrap(url); } else { - url += `query=${encodedQuery}`; + return this.getVersionAndExtensionFilters(buildNumber).then(filters => { + url += `&$filter=${filters.join(' or ')}`; + return url; + }); + } + } else { + url += `query=${encodedQuery}`; - if (buildNumber) { - url += `&build=${buildNumber}`; - } + if (buildNumber) { + url += `&build=${buildNumber}`; + } + } + + return TPromise.wrap(url); + } + + private getVersionAndExtensionFilters(buildNumber?: number): TPromise { + return this.installedExtensions.then(exts => { + const filters = exts.map(ext => { + const uuid = ext.identifier.uuid; + const versionString = ext.manifest.version + .split('.') + .map(versionPart => strings.pad(versionPart, 10)) + .join(''); + + return `(packageid eq '${uuid}' and startbuildno le '${versionString}' and endbuildno ge '${versionString}')`; + }); + + if (buildNumber) { + filters.push(`(packageid eq 'core' and startbuildno le '${buildNumber}' and endbuildno ge '${buildNumber}')`); } - return url; + return filters; }); } } const API_VERSION = 'api-version=2016-09-01-Preview'; +const QUERY_TYPE = 'querytype=full'; function escapeSpecialChars(query: string): string { return query.replace(/\./g, ' ') From 0724266efc5af0d13fe6b40a56920b39bf0e84d8 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Fri, 19 Jan 2018 16:54:06 -0800 Subject: [PATCH 023/128] Show section for settings from new extensions, with decorations on each --- .../preferences/browser/media/preferences.css | 7 ++ .../preferences/browser/preferencesEditor.ts | 23 +++--- .../browser/preferencesRenderers.ts | 47 ++++++++++++ .../parts/preferences/common/preferences.ts | 11 ++- .../preferences/common/preferencesModels.ts | 4 +- .../electron-browser/preferencesSearch.ts | 71 +++++++++++++------ 6 files changed, 131 insertions(+), 32 deletions(-) diff --git a/src/vs/workbench/parts/preferences/browser/media/preferences.css b/src/vs/workbench/parts/preferences/browser/media/preferences.css index 04251e95105..e3832b44473 100644 --- a/src/vs/workbench/parts/preferences/browser/media/preferences.css +++ b/src/vs/workbench/parts/preferences/browser/media/preferences.css @@ -276,6 +276,13 @@ cursor: pointer; } +.monaco-editor .newExtensionInstall { + background: url('info.svg') center center no-repeat; + width: 16px; + height: 16px; + cursor: pointer; +} + .monaco-editor .edit-preferences-widget.hidden { display: none; visibility: hidden; diff --git a/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts b/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts index d6e0b6c55cb..d8f1a411ebf 100644 --- a/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts +++ b/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts @@ -436,8 +436,8 @@ class PreferencesRenderersController extends Disposable { this._currentNewExtensionsSearchProvider = (updateCurrentResults && this._currentNewExtensionsSearchProvider) || this.preferencesSearchService.getRemoteSearchProvider(query, true); this._remoteFiltersInProgress = [ - this.filterOrSearchPreferences(query, this._currentNewExtensionsSearchProvider, 'newExtensionsResult', nls.localize('newExtensionsResult', "Other Extension Results")), - this.filterOrSearchPreferences(query, this._currentRemoteSearchProvider, 'nlpResult', nls.localize('nlpResult', "Natural Language Results")) + this.filterOrSearchPreferences(query, this._currentRemoteSearchProvider, 'nlpResult', nls.localize('nlpResult', "Natural Language Results"), 1), + this.filterOrSearchPreferences(query, this._currentNewExtensionsSearchProvider, 'newExtensionsResult', nls.localize('newExtensionsResult', "Other Extension Results"), 2) ]; return TPromise.join(this._remoteFiltersInProgress).then(() => { @@ -453,16 +453,18 @@ class PreferencesRenderersController extends Disposable { localFilterPreferences(query: string, updateCurrentResults?: boolean): TPromise { this._currentLocalSearchProvider = (updateCurrentResults && this._currentLocalSearchProvider) || this.preferencesSearchService.getLocalSearchProvider(query); - return this.filterOrSearchPreferences(query, this._currentLocalSearchProvider, 'filterResult', nls.localize('filterResult', "Filtered Results")); + return this.filterOrSearchPreferences(query, this._currentLocalSearchProvider, 'filterResult', nls.localize('filterResult', "Filtered Results"), 0); } - private filterOrSearchPreferences(query: string, searchProvider: ISearchProvider, groupId: string, groupLabel: string): TPromise { + private filterOrSearchPreferences(query: string, searchProvider: ISearchProvider, groupId: string, groupLabel: string, groupOrder: number, newExtensionsOnly?: boolean): TPromise { this._lastQuery = query; - return TPromise.join([ - this._filterOrSearchPreferences(query, this.defaultPreferencesRenderer, searchProvider, groupId, groupLabel), - this._filterOrSearchPreferences(query, this.editablePreferencesRenderer, searchProvider, groupId, groupLabel)] - ).then(results => { + const filterPs = [this._filterOrSearchPreferences(query, this.defaultPreferencesRenderer, searchProvider, groupId, groupLabel, groupOrder)]; + if (!newExtensionsOnly) { + filterPs.push(this._filterOrSearchPreferences(query, this.defaultPreferencesRenderer, searchProvider, groupId, groupLabel, groupOrder)); + } + + return TPromise.join(filterPs).then(results => { const [defaultFilterResult, editableFilterResult] = results; this.consolidateAndUpdate(defaultFilterResult, editableFilterResult); @@ -485,7 +487,7 @@ class PreferencesRenderersController extends Disposable { this._focusPreference(setting, this._editablePreferencesRenderer); } - private _filterOrSearchPreferences(filter: string, preferencesRenderer: IPreferencesRenderer, provider: ISearchProvider, groupId: string, groupLabel: string): TPromise { + private _filterOrSearchPreferences(filter: string, preferencesRenderer: IPreferencesRenderer, provider: ISearchProvider, groupId: string, groupLabel: string, groupOrder: number): TPromise { if (preferencesRenderer) { const model = preferencesRenderer.preferencesModel; const searchP = provider ? provider.searchModel(model) : TPromise.wrap(null); @@ -509,7 +511,8 @@ class PreferencesRenderersController extends Disposable { model.updateResultGroup(groupId, { id: groupId, label: groupLabel, - result: searchResult + result: searchResult, + order: groupOrder }) : model.updateResultGroup(groupId, null); diff --git a/src/vs/workbench/parts/preferences/browser/preferencesRenderers.ts b/src/vs/workbench/parts/preferences/browser/preferencesRenderers.ts index 49d6784e666..e41f688e6a9 100644 --- a/src/vs/workbench/parts/preferences/browser/preferencesRenderers.ts +++ b/src/vs/workbench/parts/preferences/browser/preferencesRenderers.ts @@ -6,6 +6,7 @@ import { TPromise } from 'vs/base/common/winjs.base'; import * as nls from 'vs/nls'; import { Delayer } from 'vs/base/common/async'; +import * as arrays from 'vs/base/common/arrays'; import * as strings from 'vs/base/common/strings'; import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle'; import { IAction } from 'vs/base/common/actions'; @@ -255,6 +256,7 @@ export class DefaultSettingsRenderer extends Disposable implements IPreferencesR private editSettingActionRenderer: EditSettingRenderer; private feedbackWidgetRenderer: FeedbackWidgetRenderer; private bracesHidingRenderer: BracesHidingRenderer; + private extensionCodelensRenderer: ExtensionCodelensRenderer; private filterResult: IFilterResult; private _onUpdatePreference: Emitter<{ key: string, value: any, source: IIndexedSetting }> = new Emitter<{ key: string, value: any, source: IIndexedSetting }>(); @@ -279,6 +281,7 @@ export class DefaultSettingsRenderer extends Disposable implements IPreferencesR this.feedbackWidgetRenderer = this._register(instantiationService.createInstance(FeedbackWidgetRenderer, editor)); this.bracesHidingRenderer = this._register(instantiationService.createInstance(BracesHidingRenderer, editor, preferencesModel)); this.hiddenAreasRenderer = this._register(instantiationService.createInstance(HiddenAreasRenderer, editor, [this.settingsGroupTitleRenderer, this.filteredMatchesRenderer, this.bracesHidingRenderer])); + this.extensionCodelensRenderer = this._register(instantiationService.createInstance(ExtensionCodelensRenderer, editor)); this._register(this.editSettingActionRenderer.onUpdateSetting(e => this._onUpdatePreference.fire(e))); this._register(this.settingsGroupTitleRenderer.onHiddenAreasChanged(() => this.hiddenAreasRenderer.render())); @@ -314,6 +317,7 @@ export class DefaultSettingsRenderer extends Disposable implements IPreferencesR this.settingHighlighter.clear(true); this.bracesHidingRenderer.render(filterResult, this.preferencesModel.settingsGroups); this.editSettingActionRenderer.render(filterResult.filteredGroups, this._associatedPreferencesModel); + this.extensionCodelensRenderer.render(filterResult); } else { this.settingHighlighter.clear(true); this.filteredMatchesRenderer.render(null, this.preferencesModel.settingsGroups); @@ -323,6 +327,7 @@ export class DefaultSettingsRenderer extends Disposable implements IPreferencesR this.settingsGroupTitleRenderer.showGroup(0); this.bracesHidingRenderer.render(null, this.preferencesModel.settingsGroups); this.editSettingActionRenderer.render(this.preferencesModel.settingsGroups, this._associatedPreferencesModel); + this.extensionCodelensRenderer.render(null); } this.hiddenAreasRenderer.render(); @@ -836,6 +841,48 @@ export class HighlightMatchesRenderer extends Disposable { } } +export class ExtensionCodelensRenderer extends Disposable { + private decorationIds: string[] = []; + + constructor(private editor: ICodeEditor) { + super(); + } + + public render(filterResult: IFilterResult): void { + this.editor.changeDecorations(changeAccessor => { + this.decorationIds = changeAccessor.deltaDecorations(this.decorationIds, []); + }); + + const newExtensionGroup = filterResult && arrays.first(filterResult.filteredGroups, g => g.id === 'newExtensionsResult'); + if (newExtensionGroup) { + this.editor.changeDecorations(changeAccessor => { + const settings = newExtensionGroup.sections[0].settings; + this.decorationIds = changeAccessor.deltaDecorations(this.decorationIds, settings.map(setting => this.createDecoration(setting))); + }); + } + } + + private createDecoration(setting: ISetting): IModelDeltaDecoration { + return { + range: new Range(setting.keyRange.startLineNumber, 1, setting.keyRange.endLineNumber, 1), + options: { + glyphMarginClassName: 'newExtensionInstall', + stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, + } + }; + } + + public dispose() { + if (this.decorationIds) { + this.decorationIds = this.editor.changeDecorations(changeAccessor => { + return changeAccessor.deltaDecorations(this.decorationIds, []); + }); + } + + super.dispose(); + } +} + export interface IIndexedSetting extends ISetting { index: number; groupId: string; diff --git a/src/vs/workbench/parts/preferences/common/preferences.ts b/src/vs/workbench/parts/preferences/common/preferences.ts index d4ba6f83a05..ff7620dee0f 100644 --- a/src/vs/workbench/parts/preferences/common/preferences.ts +++ b/src/vs/workbench/parts/preferences/common/preferences.ts @@ -65,6 +65,7 @@ export interface ISearchResultGroup { id: string; label: string; result: ISearchResult; + order: number; } export interface IFilterResult { @@ -82,7 +83,15 @@ export interface ISettingMatch { } export interface IScoredResults { - [key: string]: number; + [key: string]: IRemoteSetting; +} + +export interface IRemoteSetting { + score: number; + key: string; + defaultValue: string; + description: string; + packageId: string; } export interface IFilterMetadata { diff --git a/src/vs/workbench/parts/preferences/common/preferencesModels.ts b/src/vs/workbench/parts/preferences/common/preferencesModels.ts index 18b63f4e9c0..e9e5d435af2 100644 --- a/src/vs/workbench/parts/preferences/common/preferencesModels.ts +++ b/src/vs/workbench/parts/preferences/common/preferencesModels.ts @@ -611,7 +611,9 @@ export class DefaultSettingsEditorModel extends AbstractSettingsModel implements protected update(): IFilterResult { // Grab current result groups, only render non-empty groups - const resultGroups = map.values(this._currentResultGroups); + const resultGroups = map + .values(this._currentResultGroups) + .sort((a, b) => a.order - b.order); const nonEmptyResultGroups = resultGroups.filter(group => group.result.filterMatches.length); const startLine = tail(this.settingsGroups).range.endLineNumber + 2; diff --git a/src/vs/workbench/parts/preferences/electron-browser/preferencesSearch.ts b/src/vs/workbench/parts/preferences/electron-browser/preferencesSearch.ts index 3c56eac7ff6..e1ed15f5bd7 100644 --- a/src/vs/workbench/parts/preferences/electron-browser/preferencesSearch.ts +++ b/src/vs/workbench/parts/preferences/electron-browser/preferencesSearch.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { TPromise } from 'vs/base/common/winjs.base'; -import { ISettingsEditorModel, ISetting, ISettingsGroup, IWorkbenchSettingsConfiguration, IFilterMetadata, IPreferencesSearchService, ISearchResult, ISearchProvider, IGroupFilter, ISettingMatcher, IScoredResults } from 'vs/workbench/parts/preferences/common/preferences'; +import { ISettingsEditorModel, ISetting, ISettingsGroup, IWorkbenchSettingsConfiguration, IFilterMetadata, IPreferencesSearchService, ISearchResult, ISearchProvider, IGroupFilter, ISettingMatcher, IScoredResults, ISettingMatch, IRemoteSetting } from 'vs/workbench/parts/preferences/common/preferences'; import { IRange } from 'vs/editor/common/core/range'; import { distinct, top } from 'vs/base/common/arrays'; import * as strings from 'vs/base/common/strings'; @@ -131,19 +131,37 @@ export class RemoteSearchProvider implements ISearchProvider { searchModel(preferencesModel: ISettingsEditorModel): TPromise { return this._remoteSearchP.then(remoteResult => { - if (remoteResult) { - const highScoreKey = top(Object.keys(remoteResult.scoredResults), (a, b) => remoteResult.scoredResults[b] - remoteResult.scoredResults[a], 1)[0]; - const highScore = highScoreKey ? remoteResult.scoredResults[highScoreKey] : 0; - const minScore = highScore / 5; + if (!remoteResult) { + return null; + } + const resultKeys = Object.keys(remoteResult.scoredResults); + const highScoreKey = top(resultKeys, (a, b) => remoteResult.scoredResults[b].score - remoteResult.scoredResults[a].score, 1)[0]; + const highScore = highScoreKey ? remoteResult.scoredResults[highScoreKey].score : 0; + const minScore = highScore / 5; + if (this.newExtensionsOnly) { + const passingScoreKeys = resultKeys.filter(k => remoteResult.scoredResults[k].score >= minScore); + const filterMatches: ISettingMatch[] = passingScoreKeys.map(k => { + const remoteSetting = remoteResult.scoredResults[k]; + const setting = remoteSettingToISetting(remoteSetting); + return { + setting, + score: remoteSetting.score, + matches: [] // TODO + }; + }); + + return { + filterMatches, + metadata: remoteResult + }; + } else { const settingMatcher = this.getRemoteSettingMatcher(remoteResult.scoredResults, minScore, preferencesModel); const filterMatches = preferencesModel.filterSettings(this._filter, group => null, settingMatcher); return { filterMatches, metadata: remoteResult }; - } else { - return null; } }); } @@ -168,18 +186,18 @@ export class RemoteSearchProvider implements ISearchProvider { }).then((result: any) => { const timestamp = Date.now(); const duration = timestamp - start; - const suggestions = (result.value || []) - .map(r => ({ - name: r.setting || r.Setting, - score: r['@search.score'] + const remoteSettings: IRemoteSetting[] = (result.value || []) + .map(r => ({ + key: JSON.parse(r.setting || r.Setting), + defaultValue: r['value'], + score: r['@search.score'], + description: JSON.parse(r['details']), + packageId: r['packageid'] })); const scoredResults = Object.create(null); - suggestions.forEach(s => { - const name = s.name - .replace(/^"/, '') - .replace(/"$/, ''); - scoredResults[name] = s.score; + remoteSettings.forEach(s => { + scoredResults[s.key] = s; }); return { @@ -195,10 +213,10 @@ export class RemoteSearchProvider implements ISearchProvider { private getRemoteSettingMatcher(scoredResults: IScoredResults, minScore: number, preferencesModel: ISettingsEditorModel): ISettingMatcher { return (setting: ISetting) => { - const score = scoredResults[setting.key]; - if (typeof score === 'number' && score >= minScore) { + const remoteSetting = scoredResults[setting.key]; + if (remoteSetting && remoteSetting.score >= minScore) { const settingMatches = new SettingMatches(this._filter, setting, false, (filter, setting) => preferencesModel.findValueMatches(filter, setting)).matches; - return { matches: settingMatches, score: scoredResults[setting.key] }; + return { matches: settingMatches, score: remoteSetting.score }; } return null; @@ -271,6 +289,19 @@ function escapeSpecialChars(query: string): string { .trim(); } +function remoteSettingToISetting(remoteSetting: IRemoteSetting): ISetting { + return { + description: remoteSetting.description.split('\n'), + descriptionRanges: null, + key: remoteSetting.key, + keyRange: null, + value: remoteSetting.defaultValue, + range: null, + valueRange: null, + overrides: [] + }; +} + class SettingMatches { private readonly descriptionMatchingWords: Map = new Map(); @@ -345,7 +376,7 @@ class SettingMatches { const valueMatches = or(matchesPrefix, matchesContiguousSubString)(searchString, setting.value); valueRanges = valueMatches ? valueMatches.map(match => this.toValueRange(setting, match)) : this.getRangesForWords(words, this.valueMatchingWords, [this.keyMatchingWords, this.descriptionMatchingWords]); } else { - valueRanges = this.valuesMatcher(searchString, setting); + valueRanges = this.valuesMatcher ? this.valuesMatcher(searchString, setting) : []; } return [...descriptionRanges, ...keyRanges, ...valueRanges]; From 404970532a9e9a128ed540f65bf58ba85c6ad50f Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Fri, 19 Jan 2018 17:38:43 -0800 Subject: [PATCH 024/128] Add command to search for extension by ID --- .../parts/extensions/browser/extensionsActions.ts | 11 +++++++++++ .../preferences/browser/preferencesRenderers.ts | 12 +++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/parts/extensions/browser/extensionsActions.ts b/src/vs/workbench/parts/extensions/browser/extensionsActions.ts index 905b22e3f88..0b244b693db 100644 --- a/src/vs/workbench/parts/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/parts/extensions/browser/extensionsActions.ts @@ -1692,6 +1692,17 @@ CommandsRegistry.registerCommand('workbench.extensions.action.showExtensionsForL }); }); +CommandsRegistry.registerCommand('workbench.extensions.action.showExtensionsWithId', function (accessor: ServicesAccessor, extensionId: string) { + const viewletService = accessor.get(IViewletService); + + return viewletService.openViewlet(VIEWLET_ID, true) + .then(viewlet => viewlet as IExtensionsViewlet) + .then(viewlet => { + viewlet.search(`@id:${extensionId}`); + viewlet.focus(); + }); +}); + export const extensionButtonProminentBackground = registerColor('extensionButton.prominentBackground', { dark: '#327e36', light: '#327e36', diff --git a/src/vs/workbench/parts/preferences/browser/preferencesRenderers.ts b/src/vs/workbench/parts/preferences/browser/preferencesRenderers.ts index e41f688e6a9..edbd73470a7 100644 --- a/src/vs/workbench/parts/preferences/browser/preferencesRenderers.ts +++ b/src/vs/workbench/parts/preferences/browser/preferencesRenderers.ts @@ -34,6 +34,7 @@ import { MarkdownString } from 'vs/base/common/htmlContent'; import { overrideIdentifierFromKey, IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { ITextModel, IModelDeltaDecoration, TrackedRangeStickiness } from 'vs/editor/common/model'; +import { ICommandService } from 'vs/platform/commands/common/commands'; export interface IPreferencesRenderer extends IDisposable { readonly preferencesModel: IPreferencesEditorModel; @@ -844,7 +845,8 @@ export class HighlightMatchesRenderer extends Disposable { export class ExtensionCodelensRenderer extends Disposable { private decorationIds: string[] = []; - constructor(private editor: ICodeEditor) { + constructor(private editor: ICodeEditor, + @ICommandService private commandService: ICommandService) { super(); } @@ -860,6 +862,14 @@ export class ExtensionCodelensRenderer extends Disposable { this.decorationIds = changeAccessor.deltaDecorations(this.decorationIds, settings.map(setting => this.createDecoration(setting))); }); } + + this._register(this.editor.onMouseDown((e: IEditorMouseEvent) => { + if (e.target.type !== MouseTargetType.GUTTER_GLYPH_MARGIN) { + return; + } + + this.commandService.executeCommand('workbench.extensions.action.showExtensionsWithId', 'ms-python.python'); + })); } private createDecoration(setting: ISetting): IModelDeltaDecoration { From b618e4a25ec45394f8911922a0f6eaf0b99e238c Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Sat, 20 Jan 2018 14:20:19 +0100 Subject: [PATCH 025/128] introduce writeFileStreamAndFlush --- src/vs/base/node/extfs.ts | 160 ++++++++++++---- src/vs/base/node/pfs.ts | 2 + src/vs/base/test/node/extfs/extfs.test.ts | 220 +++++++++++++++++++++- 3 files changed, 341 insertions(+), 41 deletions(-) diff --git a/src/vs/base/node/extfs.ts b/src/vs/base/node/extfs.ts index b3a4121079e..b36dd2e9b22 100644 --- a/src/vs/base/node/extfs.ts +++ b/src/vs/base/node/extfs.ts @@ -14,6 +14,7 @@ import * as fs from 'fs'; import * as paths from 'path'; import { TPromise } from 'vs/base/common/winjs.base'; import { nfcall } from 'vs/base/common/async'; +import { Readable } from 'stream'; const loop = flow.loop; @@ -54,7 +55,7 @@ export function copy(source: string, target: string, callback: (error: Error) => } if (!stat.isDirectory()) { - return pipeFs(source, target, stat.mode & 511, callback); + return doCopyFile(source, target, stat.mode & 511, callback); } if (copiedSources[source]) { @@ -75,6 +76,38 @@ export function copy(source: string, target: string, callback: (error: Error) => }); } +function doCopyFile(source: string, target: string, mode: number, callback: (error: Error) => void): void { + const reader = fs.createReadStream(source); + const writer = fs.createWriteStream(target, { mode }); + + let finished = false; + const finish = (error?: Error) => { + if (!finished) { + finished = true; + + // in error cases, pass to callback + if (error) { + callback(error); + } + + // we need to explicitly chmod because of https://github.com/nodejs/node/issues/1104 + else { + fs.chmod(target, mode, callback); + } + } + }; + + // handle errors properly + reader.once('error', error => finish(error)); + writer.once('error', error => finish(error)); + + // we are done (underlying fd has been closed) + writer.once('close', () => finish()); + + // start piping + reader.pipe(writer); +} + export function mkdirp(path: string, mode?: number): TPromise { const mkdir = () => nfcall(fs.mkdir, path, mode) .then(null, (err: NodeJS.ErrnoException) => { @@ -88,11 +121,12 @@ export function mkdirp(path: string, mode?: number): TPromise { return TPromise.wrapError(err); }); - // is root? + // stop at root if (path === paths.dirname(path)) { return TPromise.as(true); } + // recursively mkdir return mkdir().then(null, (err: NodeJS.ErrnoException) => { if (err.code === 'ENOENT') { return mkdirp(paths.dirname(path), mode).then(mkdir); @@ -102,40 +136,6 @@ export function mkdirp(path: string, mode?: number): TPromise { }); } -function pipeFs(source: string, target: string, mode: number, callback: (error: Error) => void): void { - let callbackHandled = false; - - const readStream = fs.createReadStream(source); - const writeStream = fs.createWriteStream(target, { mode: mode }); - - const onError = (error: Error) => { - if (!callbackHandled) { - callbackHandled = true; - callback(error); - } - }; - - readStream.on('error', onError); - writeStream.on('error', onError); - - readStream.on('end', () => { - (writeStream).end(() => { // In this case the write stream is known to have an end signature with callback - if (!callbackHandled) { - callbackHandled = true; - - fs.chmod(target, mode, callback); // we need to explicitly chmod because of https://github.com/nodejs/node/issues/1104 - } - }); - }); - - // In node 0.8 there is no easy way to find out when the pipe operation has finished. As such, we use the end property = false - // so that we are in charge of calling end() on the write stream and we will be notified when the write stream is really done. - // We can do this because file streams have an end() method that allows to pass in a callback. - // In node 0.10 there is an event 'finish' emitted from the write stream that can be used. See - // https://groups.google.com/forum/?fromgroups=#!topic/nodejs/YWQ1sRoXOdI - readStream.pipe(writeStream, { end: false }); -} - // Deletes the given path by first moving it out of the workspace. This has two benefits. For one, the operation can return fast because // after the rename, the contents are out of the workspace although not yet deleted. The greater benefit however is that this operation // will fail in case any file is used by another process. fs.unlink() in node will not bail if a file unlinked is used by another process. @@ -320,15 +320,95 @@ export function mv(source: string, target: string, callback: (error: Error) => v }); } +let canFlush = true; +export function writeFileAndFlush(path: string, data: string | NodeBuffer | Readable, options: { mode?: number; flag?: string; }, callback: (error?: Error) => void): void { + options = ensureOptions(options); + + if (data instanceof Readable) { + doWriteFileStreamAndFlush(path, data, options, callback); + } else { + doWriteFileAndFlush(path, data, options, callback); + } +} + +function doWriteFileStreamAndFlush(path: string, reader: Readable, options: { mode?: number; flag?: string; }, callback: (error?: Error) => void): void { + + // finish only once + let finished = false; + const finish = (error?: Error) => { + if (!finished) { + finished = true; + + // in error cases we need to manually close streams + // if the write stream was successfully opened + if (error) { + if (isOpen) { + writer.once('close', () => callback(error)); + writer.close(); + } else { + callback(error); + } + } + + // otherwise just return without error + else { + callback(); + } + } + }; + + // create writer to target + const writer = fs.createWriteStream(path, options); + + // handle errors properly + reader.once('error', error => finish(error)); + writer.once('error', error => finish(error)); + + // save the fd for later use + let fd: number; + let isOpen: boolean; + writer.once('open', descriptor => { + fd = descriptor; + isOpen = true; + }); + + // we are done (underlying fd has been closed) + writer.once('close', () => finish()); + + // handle end event because we are in charge + reader.once('end', () => { + + // flush to disk + if (canFlush && isOpen) { + fs.fdatasync(fd, (syncError: Error) => { + + // In some exotic setups it is well possible that node fails to sync + // In that case we disable flushing and warn to the console + if (syncError) { + console.warn('[node.js fs] fdatasync is now disabled for this session because it failed: ', syncError); + canFlush = false; + } + + writer.end(); + }); + } + + // do not flush + else { + writer.end(); + } + }); + + // end: false means we are in charge of ending the streams properly + reader.pipe(writer, { end: false }); +} + // Calls fs.writeFile() followed by a fs.sync() call to flush the changes to disk // We do this in cases where we want to make sure the data is really on disk and // not in some cache. // // See https://github.com/nodejs/node/blob/v5.10.0/lib/fs.js#L1194 -let canFlush = true; -export function writeFileAndFlush(path: string, data: string | NodeBuffer, options: { mode?: number; flag?: string; }, callback: (error: Error) => void): void { - options = ensureOptions(options); - +function doWriteFileAndFlush(path: string, data: string | NodeBuffer, options: { mode?: number; flag?: string; }, callback: (error?: Error) => void): void { if (!canFlush) { return fs.writeFile(path, data, options, callback); } diff --git a/src/vs/base/node/pfs.ts b/src/vs/base/node/pfs.ts index b949384de1e..ba1ab12a7ca 100644 --- a/src/vs/base/node/pfs.ts +++ b/src/vs/base/node/pfs.ts @@ -13,6 +13,7 @@ import * as fs from 'fs'; import * as os from 'os'; import * as platform from 'vs/base/common/platform'; import { once } from 'vs/base/common/event'; +import { Readable } from 'stream'; export function readdir(path: string): TPromise { return nfcall(extfs.readdir, path); @@ -101,6 +102,7 @@ const writeFilePathQueue: { [path: string]: Queue } = Object.create(null); export function writeFile(path: string, data: string, options?: { mode?: number; flag?: string; }): TPromise; export function writeFile(path: string, data: NodeBuffer, options?: { mode?: number; flag?: string; }): TPromise; +export function writeFile(path: string, data: Readable, options?: { mode?: number; flag?: string; }): TPromise; export function writeFile(path: string, data: any, options?: { mode?: number; flag?: string; }): TPromise { let queueKey = toQueueKey(path); diff --git a/src/vs/base/test/node/extfs/extfs.test.ts b/src/vs/base/test/node/extfs/extfs.test.ts index 06ecf0b8ba0..2e3e8b848ae 100644 --- a/src/vs/base/test/node/extfs/extfs.test.ts +++ b/src/vs/base/test/node/extfs/extfs.test.ts @@ -15,6 +15,7 @@ import uuid = require('vs/base/common/uuid'); import strings = require('vs/base/common/strings'); import extfs = require('vs/base/node/extfs'); import { onError } from 'vs/base/test/common/utils'; +import { Readable } from 'stream'; const ignore = () => { }; @@ -22,6 +23,37 @@ const mkdirp = (path: string, mode: number, callback: (error) => void) => { extfs.mkdirp(path, mode).done(() => callback(null), error => callback(error)); }; +const chunkSize = 64 * 1024; +const readError = 'Error while reading'; +function toReadable(value: string, throwError?: boolean): Readable { + const totalChunks = Math.ceil(value.length / chunkSize); + const stringChunks: string[] = []; + + for (let i = 0, j = 0; i < totalChunks; ++i, j += chunkSize) { + stringChunks[i] = value.substr(j, chunkSize); + } + + let counter = 0; + return new Readable({ + read: function () { + if (throwError) { + this.emit('error', new Error(readError)); + } + + let res: string; + let canPush = true; + while (canPush && (res = stringChunks[counter++])) { + canPush = this.push(res); + } + + // EOS + if (!res) { + this.push(null); + } + } + }); +} + suite('Extfs', () => { test('mkdirp', function (done: () => void) { @@ -174,7 +206,7 @@ suite('Extfs', () => { } }); - test('writeFileAndFlush', function (done: () => void) { + test('writeFileAndFlush (string)', function (done: () => void) { const id = uuid.generateUuid(); const parentDir = path.join(os.tmpdir(), 'vsctests', id); const newDir = path.join(parentDir, 'extfs', id); @@ -209,6 +241,192 @@ suite('Extfs', () => { }); }); + test('writeFileAndFlush (stream)', function (done: () => void) { + const id = uuid.generateUuid(); + const parentDir = path.join(os.tmpdir(), 'vsctests', id); + const newDir = path.join(parentDir, 'extfs', id); + const testFile = path.join(newDir, 'flushed.txt'); + + mkdirp(newDir, 493, error => { + if (error) { + return onError(error, done); + } + + assert.ok(fs.existsSync(newDir)); + + extfs.writeFileAndFlush(testFile, toReadable('Hello World'), null, error => { + if (error) { + return onError(error, done); + } + + assert.equal(fs.readFileSync(testFile), 'Hello World'); + + const largeString = (new Array(100 * 1024)).join('Large String\n'); + + extfs.writeFileAndFlush(testFile, toReadable(largeString), null, error => { + if (error) { + return onError(error, done); + } + + assert.equal(fs.readFileSync(testFile), largeString); + + extfs.del(parentDir, os.tmpdir(), done, ignore); + }); + }); + }); + }); + + test('writeFileAndFlush (file stream)', function (done: () => void) { + const id = uuid.generateUuid(); + const parentDir = path.join(os.tmpdir(), 'vsctests', id); + const sourceFile = require.toUrl('./fixtures/index.html'); + const newDir = path.join(parentDir, 'extfs', id); + const testFile = path.join(newDir, 'flushed.txt'); + + mkdirp(newDir, 493, error => { + if (error) { + return onError(error, done); + } + + assert.ok(fs.existsSync(newDir)); + + extfs.writeFileAndFlush(testFile, fs.createReadStream(sourceFile), null, error => { + if (error) { + return onError(error, done); + } + + assert.equal(fs.readFileSync(testFile).toString(), fs.readFileSync(sourceFile).toString()); + + extfs.del(parentDir, os.tmpdir(), done, ignore); + }); + }); + }); + + test('writeFileAndFlush (string, error handling)', function (done: () => void) { + const id = uuid.generateUuid(); + const parentDir = path.join(os.tmpdir(), 'vsctests', id); + const newDir = path.join(parentDir, 'extfs', id); + const testFile = path.join(newDir, 'flushed.txt'); + + mkdirp(newDir, 493, error => { + if (error) { + return onError(error, done); + } + + assert.ok(fs.existsSync(newDir)); + + fs.mkdirSync(testFile); // this will trigger an error because testFile is now a directory! + + extfs.writeFileAndFlush(testFile, 'Hello World', null, error => { + if (!error) { + return onError(new Error('Expected error for writing to readonly file'), done); + } + + extfs.del(parentDir, os.tmpdir(), done, ignore); + }); + }); + }); + + test('writeFileAndFlush (stream, error handling EISDIR)', function (done: () => void) { + const id = uuid.generateUuid(); + const parentDir = path.join(os.tmpdir(), 'vsctests', id); + const newDir = path.join(parentDir, 'extfs', id); + const testFile = path.join(newDir, 'flushed.txt'); + + mkdirp(newDir, 493, error => { + if (error) { + return onError(error, done); + } + + assert.ok(fs.existsSync(newDir)); + + fs.mkdirSync(testFile); // this will trigger an error because testFile is now a directory! + + extfs.writeFileAndFlush(testFile, toReadable('Hello World'), null, error => { + if (!error || (error).code !== 'EISDIR') { + return onError(new Error('Expected EISDIR error for writing to folder but got: ' + (error ? (error).code : 'no error')), done); + } + + extfs.del(parentDir, os.tmpdir(), done, ignore); + }); + }); + }); + + test('writeFileAndFlush (stream, error handling READERROR)', function (done: () => void) { + const id = uuid.generateUuid(); + const parentDir = path.join(os.tmpdir(), 'vsctests', id); + const newDir = path.join(parentDir, 'extfs', id); + const testFile = path.join(newDir, 'flushed.txt'); + + mkdirp(newDir, 493, error => { + if (error) { + return onError(error, done); + } + + assert.ok(fs.existsSync(newDir)); + + extfs.writeFileAndFlush(testFile, toReadable('Hello World', true /* throw error */), null, error => { + if (!error || error.message !== readError) { + return onError(new Error('Expected error for writing to folder'), done); + } + + extfs.del(parentDir, os.tmpdir(), done, ignore); + }); + }); + }); + + test('pasero writeFileAndFlush (stream, error handling EACCES)', function (done: () => void) { + const id = uuid.generateUuid(); + const parentDir = path.join(os.tmpdir(), 'vsctests', id); + const newDir = path.join(parentDir, 'extfs', id); + const testFile = path.join(newDir, 'flushed.txt'); + + mkdirp(newDir, 493, error => { + if (error) { + return onError(error, done); + } + + assert.ok(fs.existsSync(newDir)); + + fs.writeFileSync(testFile, ''); + fs.chmodSync(testFile, 33060); // make readonly + + extfs.writeFileAndFlush(testFile, toReadable('Hello World'), null, error => { + if (!error || !((error).code !== 'EACCES' || (error).code !== 'EPERM')) { + return onError(new Error('Expected EACCES/EPERM error for writing to folder but got: ' + (error ? (error).code : 'no error')), done); + } + + extfs.del(parentDir, os.tmpdir(), done, ignore); + }); + }); + }); + + test('writeFileAndFlush (file stream, error handling)', function (done: () => void) { + const id = uuid.generateUuid(); + const parentDir = path.join(os.tmpdir(), 'vsctests', id); + const sourceFile = require.toUrl('./fixtures/index.html'); + const newDir = path.join(parentDir, 'extfs', id); + const testFile = path.join(newDir, 'flushed.txt'); + + mkdirp(newDir, 493, error => { + if (error) { + return onError(error, done); + } + + assert.ok(fs.existsSync(newDir)); + + fs.mkdirSync(testFile); // this will trigger an error because testFile is now a directory! + + extfs.writeFileAndFlush(testFile, fs.createReadStream(sourceFile), null, error => { + if (!error) { + return onError(new Error('Expected error for writing to folder'), done); + } + + extfs.del(parentDir, os.tmpdir(), done, ignore); + }); + }); + }); + test('writeFileAndFlushSync', function (done: () => void) { const id = uuid.generateUuid(); const parentDir = path.join(os.tmpdir(), 'vsctests', id); From a43926a61e073d7a5b08ce7d297bd93cadb0fabc Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Sat, 20 Jan 2018 15:24:23 +0100 Subject: [PATCH 026/128] document update state machine --- src/vs/code/electron-main/menus.ts | 2 +- src/vs/platform/update/common/update.ts | 26 ++++++++++++++++--- .../electron-main/abstractUpdateService.ts | 6 ++--- .../electron-main/updateService.linux.ts | 6 ++--- .../parts/update/electron-browser/update.ts | 6 ++--- 5 files changed, 32 insertions(+), 14 deletions(-) diff --git a/src/vs/code/electron-main/menus.ts b/src/vs/code/electron-main/menus.ts index 5c1daf2bea3..7e389c63729 100644 --- a/src/vs/code/electron-main/menus.ts +++ b/src/vs/code/electron-main/menus.ts @@ -1057,7 +1057,7 @@ export class CodeMenu { case StateType.CheckingForUpdates: return [new MenuItem({ label: nls.localize('miCheckingForUpdates', "Checking For Updates..."), enabled: false })]; - case StateType.Available: + case StateType.AvailableForDownload: return [new MenuItem({ label: nls.localize('miDownloadUpdate', "Download Available Update"), click: () => { this.updateService.downloadUpdate(); diff --git a/src/vs/platform/update/common/update.ts b/src/vs/platform/update/common/update.ts index a80618c9ebb..8ce018cb411 100644 --- a/src/vs/platform/update/common/update.ts +++ b/src/vs/platform/update/common/update.ts @@ -19,11 +19,29 @@ export interface IUpdate { hash?: string; } +/** + * Updates are run as a state machine: + * + * Uninitialized + * ↓ + * Idle + * ↓ ↑ + * Checking for Updates → Available for Download + * ↓ + * Downloading → Ready + * ↓ ↑ + * Downloaded → Updating + * + * Available: There is an update available for download (linux). + * Ready: Code will be updated as soon as it restarts (win32, darwin). + * Donwloaded: There is an update ready to be installed in the background (win32). + */ + export enum StateType { Uninitialized = 'uninitialized', Idle = 'idle', - Available = 'available', CheckingForUpdates = 'checking for updates', + AvailableForDownload = 'available for download', Downloading = 'downloading', Downloaded = 'downloaded', Updating = 'updating', @@ -33,19 +51,19 @@ export enum StateType { export type Uninitialized = { type: StateType.Uninitialized }; export type Idle = { type: StateType.Idle }; export type CheckingForUpdates = { type: StateType.CheckingForUpdates, explicit: boolean }; -export type Available = { type: StateType.Available, update: IUpdate }; +export type AvailableForDownload = { type: StateType.AvailableForDownload, update: IUpdate }; export type Downloading = { type: StateType.Downloading, update: IUpdate }; export type Downloaded = { type: StateType.Downloaded, update: IUpdate }; export type Updating = { type: StateType.Updating, update: IUpdate }; export type Ready = { type: StateType.Ready, update: IUpdate }; -export type State = Uninitialized | Idle | CheckingForUpdates | Available | Downloading | Downloaded | Updating | Ready; +export type State = Uninitialized | Idle | CheckingForUpdates | AvailableForDownload | Downloading | Downloaded | Updating | Ready; export const State = { Uninitialized: { type: StateType.Uninitialized } as Uninitialized, Idle: { type: StateType.Idle } as Idle, CheckingForUpdates: (explicit: boolean) => ({ type: StateType.CheckingForUpdates, explicit } as CheckingForUpdates), - Available: (update: IUpdate) => ({ type: StateType.Available, update } as Available), + AvailableForDownload: (update: IUpdate) => ({ type: StateType.AvailableForDownload, update } as AvailableForDownload), Downloading: (update: IUpdate) => ({ type: StateType.Downloading, update } as Downloading), Downloaded: (update: IUpdate) => ({ type: StateType.Downloaded, update } as Downloaded), Updating: (update: IUpdate) => ({ type: StateType.Updating, update } as Updating), diff --git a/src/vs/platform/update/electron-main/abstractUpdateService.ts b/src/vs/platform/update/electron-main/abstractUpdateService.ts index ade88f2e6f5..97055d435d1 100644 --- a/src/vs/platform/update/electron-main/abstractUpdateService.ts +++ b/src/vs/platform/update/electron-main/abstractUpdateService.ts @@ -11,7 +11,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { ILifecycleService } from 'vs/platform/lifecycle/electron-main/lifecycleMain'; import product from 'vs/platform/node/product'; import { TPromise } from 'vs/base/common/winjs.base'; -import { IUpdateService, State, StateType, Available } from 'vs/platform/update/common/update'; +import { IUpdateService, State, StateType, AvailableForDownload } from 'vs/platform/update/common/update'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { ILogService } from 'vs/platform/log/common/log'; @@ -97,14 +97,14 @@ export abstract class AbstractUpdateService implements IUpdateService { } downloadUpdate(): TPromise { - if (this.state.type !== StateType.Available) { + if (this.state.type !== StateType.AvailableForDownload) { return TPromise.as(null); } return this.doDownloadUpdate(this.state); } - protected doDownloadUpdate(state: Available): TPromise { + protected doDownloadUpdate(state: AvailableForDownload): TPromise { return TPromise.as(null); } diff --git a/src/vs/platform/update/electron-main/updateService.linux.ts b/src/vs/platform/update/electron-main/updateService.linux.ts index a59e24fd722..53cb9a6ae9b 100644 --- a/src/vs/platform/update/electron-main/updateService.linux.ts +++ b/src/vs/platform/update/electron-main/updateService.linux.ts @@ -8,7 +8,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ILifecycleService } from 'vs/platform/lifecycle/electron-main/lifecycleMain'; import { IRequestService } from 'vs/platform/request/node/request'; -import { State, IUpdate, Available } from 'vs/platform/update/common/update'; +import { State, IUpdate, AvailableForDownload } from 'vs/platform/update/common/update'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { ILogService } from 'vs/platform/log/common/log'; @@ -57,7 +57,7 @@ export class LinuxUpdateService extends AbstractUpdateService { this.setState(State.Idle); } else { - this.setState(State.Available(update)); + this.setState(State.AvailableForDownload(update)); } }) .then(null, err => { @@ -73,7 +73,7 @@ export class LinuxUpdateService extends AbstractUpdateService { }); } - protected doDownloadUpdate(state: Available): TPromise { + protected doDownloadUpdate(state: AvailableForDownload): TPromise { shell.openExternal(state.update.url); this.setState(State.Idle); diff --git a/src/vs/workbench/parts/update/electron-browser/update.ts b/src/vs/workbench/parts/update/electron-browser/update.ts index e091ce43563..2c705a72974 100644 --- a/src/vs/workbench/parts/update/electron-browser/update.ts +++ b/src/vs/workbench/parts/update/electron-browser/update.ts @@ -338,7 +338,7 @@ export class UpdateContribution implements IGlobalActivity { } break; - case StateType.Available: + case StateType.AvailableForDownload: this.onUpdateAvailable(state.update); break; @@ -357,7 +357,7 @@ export class UpdateContribution implements IGlobalActivity { let badge: IBadge | undefined = undefined; - if (state.type === StateType.Available || state.type === StateType.Downloaded || state.type === StateType.Ready) { + if (state.type === StateType.AvailableForDownload || state.type === StateType.Downloaded || state.type === StateType.Ready) { badge = new NumberBadge(1, () => nls.localize('updateIsReady', "New {0} update available.", product.nameShort)); } else if (state.type === StateType.CheckingForUpdates || state.type === StateType.Downloading || state.type === StateType.Updating) { badge = new ProgressBadge(() => nls.localize('updateIsReady', "New {0} update available.", product.nameShort)); @@ -491,7 +491,7 @@ export class UpdateContribution implements IGlobalActivity { case StateType.CheckingForUpdates: return new Action('update.checking', nls.localize('checkingForUpdates', "Checking For Updates..."), undefined, false); - case StateType.Available: + case StateType.AvailableForDownload: return new Action('update.downloadNow', nls.localize('download now', "Download Now"), null, true, () => this.updateService.downloadUpdate()); From c7da35f1de3d342d12143536a66f43769c9fc9f9 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Sat, 20 Jan 2018 18:10:15 +0100 Subject: [PATCH 027/128] first cut stream support for updateContent --- src/typings/iconv-lite.d.ts | 6 +-- src/vs/base/node/encoding.ts | 10 ++-- src/vs/base/node/extfs.ts | 11 ++-- src/vs/base/node/pfs.ts | 3 +- src/vs/base/test/node/extfs/extfs.test.ts | 3 +- src/vs/platform/files/common/files.ts | 15 +++++- .../common/editor/textEditorModel.ts | 10 ++++ .../files/electron-browser/fileService.ts | 4 +- .../electron-browser/remoteFileService.ts | 9 ++-- .../services/files/node/fileService.ts | 50 ++++++++++++++++--- .../textfile/common/textFileEditorModel.ts | 2 +- .../workbench/test/workbenchTestServices.ts | 4 +- 12 files changed, 94 insertions(+), 33 deletions(-) diff --git a/src/typings/iconv-lite.d.ts b/src/typings/iconv-lite.d.ts index 84c54e320cf..5ad19bb95b7 100644 --- a/src/typings/iconv-lite.d.ts +++ b/src/typings/iconv-lite.d.ts @@ -6,13 +6,13 @@ /// declare module 'iconv-lite' { - export function decode(buffer: NodeBuffer, encoding: string, options?: any): string; + export function decode(buffer: NodeBuffer, encoding: string): string; - export function encode(content: string, encoding: string, options?: any): NodeBuffer; + export function encode(content: string, encoding: string, options?: { addBOM?: boolean }): NodeBuffer; export function encodingExists(encoding: string): boolean; export function decodeStream(encoding: string): NodeJS.ReadWriteStream; - export function encodeStream(encoding: string): NodeJS.ReadWriteStream; + export function encodeStream(encoding: string, options?: { addBOM?: boolean }): NodeJS.ReadWriteStream; } \ No newline at end of file diff --git a/src/vs/base/node/encoding.ts b/src/vs/base/node/encoding.ts index 4177f3ffab5..1d134c65767 100644 --- a/src/vs/base/node/encoding.ts +++ b/src/vs/base/node/encoding.ts @@ -28,11 +28,11 @@ export function bomLength(encoding: string): number { return 0; } -export function decode(buffer: NodeBuffer, encoding: string, options?: any): string { - return iconv.decode(buffer, toNodeEncoding(encoding), options); +export function decode(buffer: NodeBuffer, encoding: string): string { + return iconv.decode(buffer, toNodeEncoding(encoding)); } -export function encode(content: string, encoding: string, options?: any): NodeBuffer { +export function encode(content: string, encoding: string, options?: { addBOM?: boolean }): NodeBuffer { return iconv.encode(content, toNodeEncoding(encoding), options); } @@ -44,6 +44,10 @@ export function decodeStream(encoding: string): NodeJS.ReadWriteStream { return iconv.decodeStream(toNodeEncoding(encoding)); } +export function encodeStream(encoding: string, options?: { addBOM?: boolean }): NodeJS.ReadWriteStream { + return iconv.encodeStream(toNodeEncoding(encoding), options); +} + function toNodeEncoding(enc: string): string { if (enc === UTF8_with_bom) { return UTF8; // iconv does not distinguish UTF 8 with or without BOM, so we need to help it diff --git a/src/vs/base/node/extfs.ts b/src/vs/base/node/extfs.ts index b36dd2e9b22..9b540108284 100644 --- a/src/vs/base/node/extfs.ts +++ b/src/vs/base/node/extfs.ts @@ -14,7 +14,6 @@ import * as fs from 'fs'; import * as paths from 'path'; import { TPromise } from 'vs/base/common/winjs.base'; import { nfcall } from 'vs/base/common/async'; -import { Readable } from 'stream'; const loop = flow.loop; @@ -321,17 +320,17 @@ export function mv(source: string, target: string, callback: (error: Error) => v } let canFlush = true; -export function writeFileAndFlush(path: string, data: string | NodeBuffer | Readable, options: { mode?: number; flag?: string; }, callback: (error?: Error) => void): void { +export function writeFileAndFlush(path: string, data: string | NodeBuffer | NodeJS.ReadableStream, options: { mode?: number; flag?: string; }, callback: (error?: Error) => void): void { options = ensureOptions(options); - if (data instanceof Readable) { - doWriteFileStreamAndFlush(path, data, options, callback); - } else { + if (typeof data === 'string' || Buffer.isBuffer(data)) { doWriteFileAndFlush(path, data, options, callback); + } else { + doWriteFileStreamAndFlush(path, data, options, callback); } } -function doWriteFileStreamAndFlush(path: string, reader: Readable, options: { mode?: number; flag?: string; }, callback: (error?: Error) => void): void { +function doWriteFileStreamAndFlush(path: string, reader: NodeJS.ReadableStream, options: { mode?: number; flag?: string; }, callback: (error?: Error) => void): void { // finish only once let finished = false; diff --git a/src/vs/base/node/pfs.ts b/src/vs/base/node/pfs.ts index ba1ab12a7ca..9ebc819fd02 100644 --- a/src/vs/base/node/pfs.ts +++ b/src/vs/base/node/pfs.ts @@ -13,7 +13,6 @@ import * as fs from 'fs'; import * as os from 'os'; import * as platform from 'vs/base/common/platform'; import { once } from 'vs/base/common/event'; -import { Readable } from 'stream'; export function readdir(path: string): TPromise { return nfcall(extfs.readdir, path); @@ -102,7 +101,7 @@ const writeFilePathQueue: { [path: string]: Queue } = Object.create(null); export function writeFile(path: string, data: string, options?: { mode?: number; flag?: string; }): TPromise; export function writeFile(path: string, data: NodeBuffer, options?: { mode?: number; flag?: string; }): TPromise; -export function writeFile(path: string, data: Readable, options?: { mode?: number; flag?: string; }): TPromise; +export function writeFile(path: string, data: NodeJS.ReadableStream, options?: { mode?: number; flag?: string; }): TPromise; export function writeFile(path: string, data: any, options?: { mode?: number; flag?: string; }): TPromise { let queueKey = toQueueKey(path); diff --git a/src/vs/base/test/node/extfs/extfs.test.ts b/src/vs/base/test/node/extfs/extfs.test.ts index 2e3e8b848ae..686934d9e01 100644 --- a/src/vs/base/test/node/extfs/extfs.test.ts +++ b/src/vs/base/test/node/extfs/extfs.test.ts @@ -50,7 +50,8 @@ function toReadable(value: string, throwError?: boolean): Readable { if (!res) { this.push(null); } - } + }, + encoding: 'utf8' }); } diff --git a/src/vs/platform/files/common/files.ts b/src/vs/platform/files/common/files.ts index af89c431ab6..0fe4bf63718 100644 --- a/src/vs/platform/files/common/files.ts +++ b/src/vs/platform/files/common/files.ts @@ -83,7 +83,7 @@ export interface IFileService { /** * Updates the content replacing its previous value. */ - updateContent(resource: URI, value: string, options?: IUpdateContentOptions): TPromise; + updateContent(resource: URI, value: string | ITextSnapshot, options?: IUpdateContentOptions): TPromise; /** * Moves the file to a new path identified by the resource. @@ -468,6 +468,19 @@ export interface ITextSnapshot { read(): string; } +/** + * Helper method to convert a snapshot into its full string form. + */ +export function snapshotToString(snapshot: ITextSnapshot): string { + const chunks: string[] = []; + let chunk: string; + while (typeof (chunk = snapshot.read()) === 'string') { + chunks.push(chunk); + } + + return chunks.join(''); +} + /** * Streamable content and meta information of a file. */ diff --git a/src/vs/workbench/common/editor/textEditorModel.ts b/src/vs/workbench/common/editor/textEditorModel.ts index 524ffbdf8d6..29550c7197f 100644 --- a/src/vs/workbench/common/editor/textEditorModel.ts +++ b/src/vs/workbench/common/editor/textEditorModel.ts @@ -13,6 +13,7 @@ import { ITextEditorModel } from 'vs/editor/common/services/resolverService'; import { IModeService } from 'vs/editor/common/services/modeService'; import { IModelService } from 'vs/editor/common/services/modelService'; import { IDisposable } from 'vs/base/common/lifecycle'; +import { ITextSnapshot } from 'vs/platform/files/common/files'; /** * The base text editor model leverages the code editor model. This class is only intended to be subclassed and not instantiated. @@ -142,6 +143,15 @@ export abstract class BaseTextEditorModel extends EditorModel implements ITextEd return null; } + public createSnapshot(): ITextSnapshot { + const model = this.textEditorModel; + if (model) { + return model.createSnapshot(true /* Preserve BOM */); + } + + return null; + } + public isResolved(): boolean { return !!this.textEditorModelHandle; } diff --git a/src/vs/workbench/services/files/electron-browser/fileService.ts b/src/vs/workbench/services/files/electron-browser/fileService.ts index 5eeae64bf3a..e97cc0ffd1d 100644 --- a/src/vs/workbench/services/files/electron-browser/fileService.ts +++ b/src/vs/workbench/services/files/electron-browser/fileService.ts @@ -11,7 +11,7 @@ import paths = require('vs/base/common/paths'); import encoding = require('vs/base/node/encoding'); import errors = require('vs/base/common/errors'); import uri from 'vs/base/common/uri'; -import { FileOperation, FileOperationEvent, IFileService, IFilesConfiguration, IResolveFileOptions, IFileStat, IResolveFileResult, IContent, IStreamContent, IImportResult, IResolveContentOptions, IUpdateContentOptions, FileChangesEvent, ICreateFileOptions } from 'vs/platform/files/common/files'; +import { FileOperation, FileOperationEvent, IFileService, IFilesConfiguration, IResolveFileOptions, IFileStat, IResolveFileResult, IContent, IStreamContent, IImportResult, IResolveContentOptions, IUpdateContentOptions, FileChangesEvent, ICreateFileOptions, ITextSnapshot } from 'vs/platform/files/common/files'; import { FileService as NodeFileService, IFileServiceOptions, IEncodingOverride } from 'vs/workbench/services/files/node/fileService'; import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; @@ -181,7 +181,7 @@ export class FileService implements IFileService { return this.raw.resolveStreamContent(resource, options); } - public updateContent(resource: uri, value: string, options?: IUpdateContentOptions): TPromise { + public updateContent(resource: uri, value: string | ITextSnapshot, options?: IUpdateContentOptions): TPromise { return this.raw.updateContent(resource, value, options); } diff --git a/src/vs/workbench/services/files/electron-browser/remoteFileService.ts b/src/vs/workbench/services/files/electron-browser/remoteFileService.ts index 2bf1ba014e1..1fb80d99042 100644 --- a/src/vs/workbench/services/files/electron-browser/remoteFileService.ts +++ b/src/vs/workbench/services/files/electron-browser/remoteFileService.ts @@ -6,7 +6,7 @@ import URI from 'vs/base/common/uri'; import { FileService } from 'vs/workbench/services/files/electron-browser/fileService'; -import { IContent, IStreamContent, IFileStat, IResolveContentOptions, IUpdateContentOptions, IResolveFileOptions, IResolveFileResult, FileOperationEvent, FileOperation, IFileSystemProvider, IStat, FileType, IImportResult, FileChangesEvent, ICreateFileOptions, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files'; +import { IContent, IStreamContent, IFileStat, IResolveContentOptions, IUpdateContentOptions, IResolveFileOptions, IResolveFileResult, FileOperationEvent, FileOperation, IFileSystemProvider, IStat, FileType, IImportResult, FileChangesEvent, ICreateFileOptions, FileOperationError, FileOperationResult, ITextSnapshot, snapshotToString } from 'vs/platform/files/common/files'; import { TPromise } from 'vs/base/common/winjs.base'; import { basename, join } from 'path'; import { IDisposable } from 'vs/base/common/lifecycle'; @@ -351,7 +351,7 @@ export class RemoteFileService extends FileService { } } - updateContent(resource: URI, value: string, options?: IUpdateContentOptions): TPromise { + updateContent(resource: URI, value: string | ITextSnapshot, options?: IUpdateContentOptions): TPromise { if (resource.scheme === Schemas.file) { return super.updateContent(resource, value, options); } else { @@ -361,9 +361,10 @@ export class RemoteFileService extends FileService { } } - private _doUpdateContent(provider: IFileSystemProvider, resource: URI, content: string, options: IUpdateContentOptions): TPromise { + private _doUpdateContent(provider: IFileSystemProvider, resource: URI, content: string | ITextSnapshot, options: IUpdateContentOptions): TPromise { const encoding = this.getEncoding(resource, options.encoding); - return provider.write(resource, encode(content, encoding)).then(() => { + // TODO@Joh support streaming API for remote file system writes + return provider.write(resource, encode(typeof content === 'string' ? content : snapshotToString(content), encoding)).then(() => { return this.resolveFile(resource); }); } diff --git a/src/vs/workbench/services/files/node/fileService.ts b/src/vs/workbench/services/files/node/fileService.ts index b1475b44eb9..d55169d2af0 100644 --- a/src/vs/workbench/services/files/node/fileService.ts +++ b/src/vs/workbench/services/files/node/fileService.ts @@ -11,7 +11,7 @@ import os = require('os'); import crypto = require('crypto'); import assert = require('assert'); -import { isParent, FileOperation, FileOperationEvent, IContent, IFileService, IResolveFileOptions, IResolveFileResult, IResolveContentOptions, IFileStat, IStreamContent, FileOperationError, FileOperationResult, IUpdateContentOptions, FileChangeType, IImportResult, FileChangesEvent, ICreateFileOptions, IContentData } from 'vs/platform/files/common/files'; +import { isParent, FileOperation, FileOperationEvent, IContent, IFileService, IResolveFileOptions, IResolveFileResult, IResolveContentOptions, IFileStat, IStreamContent, FileOperationError, FileOperationResult, IUpdateContentOptions, FileChangeType, IImportResult, FileChangesEvent, ICreateFileOptions, IContentData, ITextSnapshot, snapshotToString } from 'vs/platform/files/common/files'; import { MAX_FILE_SIZE } from 'vs/platform/files/node/files'; import { isEqualOrParent } from 'vs/base/common/paths'; import { ResourceMap } from 'vs/base/common/map'; @@ -41,6 +41,7 @@ import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cance import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { getBaseLabel } from 'vs/base/common/labels'; import { assign } from 'vs/base/common/objects'; +import { Readable } from 'stream'; export interface IEncodingOverride { resource: uri; @@ -505,15 +506,16 @@ export class FileService implements IFileService { }); } - public updateContent(resource: uri, value: string, options: IUpdateContentOptions = Object.create(null)): TPromise { + public updateContent(resource: uri, value: string | ITextSnapshot, options: IUpdateContentOptions = Object.create(null)): TPromise { if (this.options.elevationSupport && options.writeElevated) { - return this.doUpdateContentElevated(resource, value, options); + // We can currently only write strings elevated, so we need to convert snapshots properly + return this.doUpdateContentElevated(resource, typeof value === 'string' ? value : snapshotToString(value), options); } return this.doUpdateContent(resource, value, options); } - private doUpdateContent(resource: uri, value: string, options: IUpdateContentOptions = Object.create(null)): TPromise { + private doUpdateContent(resource: uri, value: string | ITextSnapshot, options: IUpdateContentOptions = Object.create(null)): TPromise { const absolutePath = this.toAbsolutePath(resource); // 1.) check file @@ -579,18 +581,25 @@ export class FileService implements IFileService { }); } - private doSetContentsAndResolve(resource: uri, absolutePath: string, value: string, addBOM: boolean, encodingToWrite: string, options?: { mode?: number; flag?: string; }): TPromise { + private doSetContentsAndResolve(resource: uri, absolutePath: string, value: string | ITextSnapshot, addBOM: boolean, encodingToWrite: string, options?: { mode?: number; flag?: string; }): TPromise { let writeFilePromise: TPromise; // Write fast if we do UTF 8 without BOM if (!addBOM && encodingToWrite === encoding.UTF8) { - writeFilePromise = pfs.writeFile(absolutePath, value, options); + if (typeof value === 'string') { + writeFilePromise = pfs.writeFile(absolutePath, value, options); + } else { + writeFilePromise = pfs.writeFile(absolutePath, this.snapshotToReadableStream(value), options); + } } // Otherwise use encoding lib else { - const encoded = encoding.encode(value, encodingToWrite, { addBOM }); - writeFilePromise = pfs.writeFile(absolutePath, encoded, options); + if (typeof value === 'string') { + writeFilePromise = pfs.writeFile(absolutePath, encoding.encode(value, encodingToWrite, { addBOM }), options); + } else { + writeFilePromise = pfs.writeFile(absolutePath, this.snapshotToReadableStream(value).pipe(encoding.encodeStream(encodingToWrite, { addBOM })), options); + } } // set contents @@ -601,6 +610,31 @@ export class FileService implements IFileService { }); } + private snapshotToReadableStream(snapshot: ITextSnapshot): NodeJS.ReadableStream { + return new Readable({ + read: function () { + try { + let chunk: string; + let canPush = true; + + // Push all chunks as long as we can push and as long as + // the underlying snapshot returns strings to us + while (canPush && typeof (chunk = snapshot.read()) === 'string') { + canPush = this.push(chunk); + } + + // Signal EOS by pushing NULL + if (typeof chunk !== 'string') { + this.push(null); + } + } catch (error) { + this.emit('error', error); + } + }, + encoding: encoding.UTF8 // very important, so that strings are passed around and not buffers! + }); + } + private doUpdateContentElevated(resource: uri, value: string, options: IUpdateContentOptions = Object.create(null)): TPromise { const absolutePath = this.toAbsolutePath(resource); diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts index fc18f6c5656..beb2ea20d74 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts @@ -702,7 +702,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // Save to Disk // mark the save operation as currently pending with the versionId (it might have changed from a save participant triggering) diag(`doSave(${versionId}) - before updateContent()`, this.resource, new Date()); - return this.saveSequentializer.setPending(newVersionId, this.fileService.updateContent(this.lastResolvedDiskStat.resource, this.getValue(), { + return this.saveSequentializer.setPending(newVersionId, this.fileService.updateContent(this.lastResolvedDiskStat.resource, this.createSnapshot(), { overwriteReadonly: options.overwriteReadonly, overwriteEncoding: options.overwriteEncoding, mtime: this.lastResolvedDiskStat.mtime, diff --git a/src/vs/workbench/test/workbenchTestServices.ts b/src/vs/workbench/test/workbenchTestServices.ts index e45bc1a4b10..e6f1b33310f 100644 --- a/src/vs/workbench/test/workbenchTestServices.ts +++ b/src/vs/workbench/test/workbenchTestServices.ts @@ -33,7 +33,7 @@ import { ServiceCollection } from 'vs/platform/instantiation/common/serviceColle import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService'; import { IEditorGroupService, GroupArrangement, GroupOrientation, IEditorTabOptions, IMoveOptions } from 'vs/workbench/services/group/common/groupService'; import { TextFileService } from 'vs/workbench/services/textfile/common/textFileService'; -import { FileOperationEvent, IFileService, IResolveContentOptions, FileOperationError, IFileStat, IResolveFileResult, IImportResult, FileChangesEvent, IResolveFileOptions, IContent, IUpdateContentOptions, IStreamContent, ICreateFileOptions } from 'vs/platform/files/common/files'; +import { FileOperationEvent, IFileService, IResolveContentOptions, FileOperationError, IFileStat, IResolveFileResult, IImportResult, FileChangesEvent, IResolveFileOptions, IContent, IUpdateContentOptions, IStreamContent, ICreateFileOptions, ITextSnapshot } from 'vs/platform/files/common/files'; import { IModelService } from 'vs/editor/common/services/modelService'; import { ModeServiceImpl } from 'vs/editor/common/services/modeServiceImpl'; import { ModelServiceImpl } from 'vs/editor/common/services/modelServiceImpl'; @@ -755,7 +755,7 @@ export class TestFileService implements IFileService { }); } - updateContent(resource: URI, value: string, options?: IUpdateContentOptions): TPromise { + updateContent(resource: URI, value: string | ITextSnapshot, options?: IUpdateContentOptions): TPromise { return TPromise.timeout(1).then(() => { return { resource, From 34ce394126df4fda096eb4d77015aa44ff4f9d3f Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Sun, 21 Jan 2018 10:43:53 +0100 Subject: [PATCH 028/128] fix fdatasync working properly --- src/vs/base/node/extfs.ts | 44 +++++++++++-------- .../services/files/node/fileService.ts | 8 ++-- 2 files changed, 28 insertions(+), 24 deletions(-) diff --git a/src/vs/base/node/extfs.ts b/src/vs/base/node/extfs.ts index 9b540108284..963a9a3905e 100644 --- a/src/vs/base/node/extfs.ts +++ b/src/vs/base/node/extfs.ts @@ -356,14 +356,13 @@ function doWriteFileStreamAndFlush(path: string, reader: NodeJS.ReadableStream, } }; - // create writer to target - const writer = fs.createWriteStream(path, options); + // create writer to target. we set autoClose: false because we want to use the streams + // file descriptor to call fs.fdatasync to ensure the data is flushed to disk + const writer = fs.createWriteStream(path, { mode: options.mode, flags: options.flag, autoClose: false }); - // handle errors properly - reader.once('error', error => finish(error)); - writer.once('error', error => finish(error)); - - // save the fd for later use + // Event: 'open' + // Purpose: save the fd for later use + // Notes: will not be called when there is an error opening the file descriptor! let fd: number; let isOpen: boolean; writer.once('open', descriptor => { @@ -371,11 +370,16 @@ function doWriteFileStreamAndFlush(path: string, reader: NodeJS.ReadableStream, isOpen = true; }); - // we are done (underlying fd has been closed) - writer.once('close', () => finish()); + // Event: 'error' + // Purpose: to return the error to the outside and to close the write stream (does not happen automatically) + reader.once('error', error => finish(error)); + writer.once('error', error => finish(error)); - // handle end event because we are in charge - reader.once('end', () => { + // Event: 'finish' + // Purpose: use fs.fdatasync to flush the contents to disk + // Notes: event is called when the writer has finished writing to the underlying resource. we must call writer.close() + // because we have created the WriteStream with autoClose: false + writer.once('finish', () => { // flush to disk if (canFlush && isOpen) { @@ -388,18 +392,20 @@ function doWriteFileStreamAndFlush(path: string, reader: NodeJS.ReadableStream, canFlush = false; } - writer.end(); + writer.close(); }); - } - - // do not flush - else { - writer.end(); + } else { + writer.close(); } }); - // end: false means we are in charge of ending the streams properly - reader.pipe(writer, { end: false }); + // Event: 'close' + // Purpose: signal we are done to the outside + // Notes: event is called when the writer's filedescriptor is closed + writer.once('close', () => finish()); + + // start data piping + reader.pipe(writer); } // Calls fs.writeFile() followed by a fs.sync() call to flush the changes to disk diff --git a/src/vs/workbench/services/files/node/fileService.ts b/src/vs/workbench/services/files/node/fileService.ts index d55169d2af0..f443fec34a4 100644 --- a/src/vs/workbench/services/files/node/fileService.ts +++ b/src/vs/workbench/services/files/node/fileService.ts @@ -10,8 +10,7 @@ import fs = require('fs'); import os = require('os'); import crypto = require('crypto'); import assert = require('assert'); - -import { isParent, FileOperation, FileOperationEvent, IContent, IFileService, IResolveFileOptions, IResolveFileResult, IResolveContentOptions, IFileStat, IStreamContent, FileOperationError, FileOperationResult, IUpdateContentOptions, FileChangeType, IImportResult, FileChangesEvent, ICreateFileOptions, IContentData, ITextSnapshot, snapshotToString } from 'vs/platform/files/common/files'; +import { isParent, FileOperation, FileOperationEvent, IContent, IFileService, IResolveFileOptions, IResolveFileResult, IResolveContentOptions, IFileStat, IStreamContent, FileOperationError, FileOperationResult, IUpdateContentOptions, FileChangeType, IImportResult, FileChangesEvent, ICreateFileOptions, IContentData, ITextSnapshot } from 'vs/platform/files/common/files'; import { MAX_FILE_SIZE } from 'vs/platform/files/node/files'; import { isEqualOrParent } from 'vs/base/common/paths'; import { ResourceMap } from 'vs/base/common/map'; @@ -508,8 +507,7 @@ export class FileService implements IFileService { public updateContent(resource: uri, value: string | ITextSnapshot, options: IUpdateContentOptions = Object.create(null)): TPromise { if (this.options.elevationSupport && options.writeElevated) { - // We can currently only write strings elevated, so we need to convert snapshots properly - return this.doUpdateContentElevated(resource, typeof value === 'string' ? value : snapshotToString(value), options); + return this.doUpdateContentElevated(resource, value, options); } return this.doUpdateContent(resource, value, options); @@ -635,7 +633,7 @@ export class FileService implements IFileService { }); } - private doUpdateContentElevated(resource: uri, value: string, options: IUpdateContentOptions = Object.create(null)): TPromise { + private doUpdateContentElevated(resource: uri, value: string | ITextSnapshot, options: IUpdateContentOptions = Object.create(null)): TPromise { const absolutePath = this.toAbsolutePath(resource); // 1.) check file From 1d56aabed51fc8324144e30289dab18b4ec5a8cd Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Sun, 21 Jan 2018 10:51:57 +0100 Subject: [PATCH 029/128] add more text snapshot tests --- .../files/test/node/fileService.test.ts | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/src/vs/workbench/services/files/test/node/fileService.test.ts b/src/vs/workbench/services/files/test/node/fileService.test.ts index 3a97a2a5cec..863f43f8721 100644 --- a/src/vs/workbench/services/files/test/node/fileService.test.ts +++ b/src/vs/workbench/services/files/test/node/fileService.test.ts @@ -22,6 +22,7 @@ import { onError } from 'vs/base/test/common/utils'; import { TestContextService, TestTextResourceConfigurationService, getRandomTestPath, TestLifecycleService } from 'vs/workbench/test/workbenchTestServices'; import { Workspace, toWorkspaceFolders } from 'vs/platform/workspace/common/workspace'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; +import { TextModel } from 'vs/editor/common/model/textModel'; suite('FileService', () => { let service: FileService; @@ -581,6 +582,54 @@ suite('FileService', () => { }, error => onError(error, done)); }); + test('updateContent (ITextSnapShot)', function (done: () => void) { + const resource = uri.file(path.join(testDir, 'small.txt')); + + service.resolveContent(resource).done(c => { + assert.equal(c.value, 'Small File'); + + const model = TextModel.createFromString('Updates to the small file'); + + return service.updateContent(c.resource, model.createSnapshot()).then(c => { + assert.equal(fs.readFileSync(resource.fsPath), 'Updates to the small file'); + + model.dispose(); + + done(); + }); + }, error => onError(error, done)); + }); + + test('updateContent (large file)', function (done: () => void) { + const resource = uri.file(path.join(testDir, 'lorem.txt')); + + service.resolveContent(resource).done(c => { + const newValue = c.value + c.value; + c.value = newValue; + + return service.updateContent(c.resource, c.value).then(c => { + assert.equal(fs.readFileSync(resource.fsPath), newValue); + + done(); + }); + }, error => onError(error, done)); + }); + + test('updateContent (large file, ITextSnapShot)', function (done: () => void) { + const resource = uri.file(path.join(testDir, 'lorem.txt')); + + service.resolveContent(resource).done(c => { + const newValue = c.value + c.value; + const model = TextModel.createFromString(newValue); + + return service.updateContent(c.resource, model.createSnapshot()).then(c => { + assert.equal(fs.readFileSync(resource.fsPath), newValue); + + done(); + }); + }, error => onError(error, done)); + }); + test('updateContent - use encoding (UTF 16 BE)', function (done: () => void) { const resource = uri.file(path.join(testDir, 'small.txt')); const encoding = 'utf16be'; @@ -602,6 +651,31 @@ suite('FileService', () => { }, error => onError(error, done)); }); + test('updateContent - use encoding (UTF 16 BE, ITextSnapShot)', function (done: () => void) { + const resource = uri.file(path.join(testDir, 'small.txt')); + const encoding = 'utf16be'; + + service.resolveContent(resource).done(c => { + c.encoding = encoding; + + const model = TextModel.createFromString(c.value); + + return service.updateContent(c.resource, model.createSnapshot(), { encoding: encoding }).then(c => { + return encodingLib.detectEncodingByBOM(c.resource.fsPath).then((enc) => { + assert.equal(enc, encodingLib.UTF16be); + + return service.resolveContent(resource).then(c => { + assert.equal(c.encoding, encoding); + + model.dispose(); + + done(); + }); + }); + }); + }, error => onError(error, done)); + }); + test('updateContent - encoding preserved (UTF 16 LE)', function (done: () => void) { const encoding = 'utf16le'; const resource = uri.file(path.join(testDir, 'some_utf16le.css')); @@ -625,6 +699,31 @@ suite('FileService', () => { }, error => onError(error, done)); }); + test('updateContent - encoding preserved (UTF 16 LE, ITextSnapShot)', function (done: () => void) { + const encoding = 'utf16le'; + const resource = uri.file(path.join(testDir, 'some_utf16le.css')); + + service.resolveContent(resource).done(c => { + assert.equal(c.encoding, encoding); + + const model = TextModel.createFromString('Some updates'); + + return service.updateContent(c.resource, model.createSnapshot(), { encoding: encoding }).then(c => { + return encodingLib.detectEncodingByBOM(c.resource.fsPath).then((enc) => { + assert.equal(enc, encodingLib.UTF16le); + + return service.resolveContent(resource).then(c => { + assert.equal(c.encoding, encoding); + + model.dispose(); + + done(); + }); + }); + }); + }, error => onError(error, done)); + }); + test('resolveContent - large file', function (done: () => void) { const resource = uri.file(path.join(testDir, 'lorem.txt')); From 7bbf60b0111f2dede4d7730d0f4f23c442245800 Mon Sep 17 00:00:00 2001 From: Anton Kosiakov Date: Sun, 21 Jan 2018 12:13:40 +0100 Subject: [PATCH 030/128] [monaco] bind ILogService to NullLogService by default --- src/vs/editor/standalone/browser/standaloneServices.ts | 3 +++ 1 file changed, 3 insertions(+) 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 { From d44201fa07b1c89ac35875f90bdc13b9b68e3b3c Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Sun, 21 Jan 2018 12:20:46 +0100 Subject: [PATCH 031/128] wire in ITextSnapshot for backup writing --- src/vs/platform/files/common/files.ts | 26 ++++++++++ .../parts/backup/common/backupModelTracker.ts | 4 +- .../files/electron-browser/fileActions.ts | 2 +- .../electron-browser/views/explorerViewer.ts | 2 +- .../services/backup/common/backup.ts | 6 +-- .../services/backup/node/backupFileService.ts | 22 +++++--- .../test/node/backupFileService.test.ts | 52 ++++++++++++++++++- .../files/test/node/fileService.test.ts | 14 +++-- .../textfile/common/textFileService.ts | 4 +- .../services/textfile/common/textfiles.ts | 4 +- .../workbench/test/workbenchTestServices.ts | 2 +- 11 files changed, 116 insertions(+), 22 deletions(-) diff --git a/src/vs/platform/files/common/files.ts b/src/vs/platform/files/common/files.ts index 0fe4bf63718..9f313e5a55a 100644 --- a/src/vs/platform/files/common/files.ts +++ b/src/vs/platform/files/common/files.ts @@ -481,6 +481,32 @@ export function snapshotToString(snapshot: ITextSnapshot): string { return chunks.join(''); } +/** + * Helper that wraps around a ITextSnapshot and allows to have a + * preamble that the read() method will return first. + */ +export class BufferedTextSnapshot implements ITextSnapshot { + private preambleHandled: boolean; + + constructor(private snapshot: ITextSnapshot, private preamble: string) { + } + + public read(): string { + let value = this.snapshot.read(); + if (!this.preambleHandled) { + this.preambleHandled = true; + + if (typeof value === 'string') { + value = this.preamble + value; + } else { + value = this.preamble; + } + } + + return value; + } +} + /** * Streamable content and meta information of a file. */ diff --git a/src/vs/workbench/parts/backup/common/backupModelTracker.ts b/src/vs/workbench/parts/backup/common/backupModelTracker.ts index 7078c5cd4b5..1c61b9715e2 100644 --- a/src/vs/workbench/parts/backup/common/backupModelTracker.ts +++ b/src/vs/workbench/parts/backup/common/backupModelTracker.ts @@ -72,14 +72,14 @@ export class BackupModelTracker implements IWorkbenchContribution { // Do not backup when auto save after delay is configured if (!this.configuredAutoSaveAfterDelay) { const model = this.textFileService.models.get(event.resource); - this.backupFileService.backupResource(model.getResource(), model.getValue(), model.getVersionId()).done(null, errors.onUnexpectedError); + this.backupFileService.backupResource(model.getResource(), model.createSnapshot(), model.getVersionId()).done(null, errors.onUnexpectedError); } } } private onUntitledModelChanged(resource: Uri): void { if (this.untitledEditorService.isDirty(resource)) { - this.untitledEditorService.loadOrCreate({ resource }).then(model => this.backupFileService.backupResource(resource, model.getValue(), model.getVersionId())).done(null, errors.onUnexpectedError); + this.untitledEditorService.loadOrCreate({ resource }).then(model => this.backupFileService.backupResource(resource, model.createSnapshot(), model.getVersionId())).done(null, errors.onUnexpectedError); } else { this.discardBackup(resource); } diff --git a/src/vs/workbench/parts/files/electron-browser/fileActions.ts b/src/vs/workbench/parts/files/electron-browser/fileActions.ts index e80a6804eb1..952968f7641 100644 --- a/src/vs/workbench/parts/files/electron-browser/fileActions.ts +++ b/src/vs/workbench/parts/files/electron-browser/fileActions.ts @@ -318,7 +318,7 @@ class RenameFileAction extends BaseRenameAction { const model = this.textFileService.models.get(d); - return this.backupFileService.backupResource(renamed, model.getValue(), model.getVersionId()); + return this.backupFileService.backupResource(renamed, model.createSnapshot(), model.getVersionId()); })) // 2. soft revert all dirty since we have backed up their contents diff --git a/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts index a812d54aaff..49550e507a1 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts @@ -968,7 +968,7 @@ export class FileDragAndDrop extends SimpleFileResourceDragAndDrop { const model = this.textFileService.models.get(d); - return this.backupFileService.backupResource(moved, model.getValue(), model.getVersionId()); + return this.backupFileService.backupResource(moved, model.createSnapshot(), model.getVersionId()); })) // 2. soft revert all dirty since we have backed up their contents diff --git a/src/vs/workbench/services/backup/common/backup.ts b/src/vs/workbench/services/backup/common/backup.ts index 7a46ffa491e..0b4d35f5d0c 100644 --- a/src/vs/workbench/services/backup/common/backup.ts +++ b/src/vs/workbench/services/backup/common/backup.ts @@ -8,7 +8,7 @@ import Uri from 'vs/base/common/uri'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { TPromise } from 'vs/base/common/winjs.base'; -import { IResolveContentOptions, IUpdateContentOptions } from 'vs/platform/files/common/files'; +import { IResolveContentOptions, IUpdateContentOptions, ITextSnapshot } from 'vs/platform/files/common/files'; import { ITextBufferFactory } from 'vs/editor/common/model'; export const IBackupFileService = createDecorator('backupFileService'); @@ -52,10 +52,10 @@ export interface IBackupFileService { * Backs up a resource. * * @param resource The resource to back up. - * @param content The content of the resource. + * @param content The content of the resource as value or snapshot. * @param versionId The version id of the resource to backup. */ - backupResource(resource: Uri, content: string, versionId?: number): TPromise; + backupResource(resource: Uri, content: string | ITextSnapshot, versionId?: number): TPromise; /** * Gets a list of file backups for the current workspace. diff --git a/src/vs/workbench/services/backup/node/backupFileService.ts b/src/vs/workbench/services/backup/node/backupFileService.ts index ff3543726ce..3ef38e18dc5 100644 --- a/src/vs/workbench/services/backup/node/backupFileService.ts +++ b/src/vs/workbench/services/backup/node/backupFileService.ts @@ -11,7 +11,7 @@ import * as pfs from 'vs/base/node/pfs'; import Uri from 'vs/base/common/uri'; import { ResourceQueue } from 'vs/base/common/async'; import { IBackupFileService, BACKUP_FILE_UPDATE_OPTIONS } from 'vs/workbench/services/backup/common/backup'; -import { IFileService } from 'vs/platform/files/common/files'; +import { IFileService, ITextSnapshot, BufferedTextSnapshot, IFileStat } from 'vs/platform/files/common/files'; import { TPromise } from 'vs/base/common/winjs.base'; import { readToMatchingString } from 'vs/base/node/stream'; import { Range } from 'vs/editor/common/core/range'; @@ -149,7 +149,7 @@ export class BackupFileService implements IBackupFileService { }); } - public backupResource(resource: Uri, content: string, versionId?: number): TPromise { + public backupResource(resource: Uri, content: string | ITextSnapshot, versionId?: number): TPromise { if (this.isShuttingDown) { return TPromise.as(void 0); } @@ -164,11 +164,21 @@ export class BackupFileService implements IBackupFileService { return void 0; // return early if backup version id matches requested one } - // Add metadata to top of file - content = `${resource.toString()}${BackupFileService.META_MARKER}${content}`; - return this.ioOperationQueues.queueFor(backupResource).queue(() => { - return this.fileService.updateContent(backupResource, content, BACKUP_FILE_UPDATE_OPTIONS).then(() => model.add(backupResource, versionId)); + const preamble = `${resource.toString()}${BackupFileService.META_MARKER}`; + + // Update content with value + let updateContentPromise: TPromise; + if (typeof content === 'string') { + updateContentPromise = this.fileService.updateContent(backupResource, `${preamble}${content}`, BACKUP_FILE_UPDATE_OPTIONS); + } + + // Update content with snapshot + else { + updateContentPromise = this.fileService.updateContent(backupResource, new BufferedTextSnapshot(content, preamble), BACKUP_FILE_UPDATE_OPTIONS); + } + + return updateContentPromise.then(() => model.add(backupResource, versionId)); }); }); } diff --git a/src/vs/workbench/services/backup/test/node/backupFileService.test.ts b/src/vs/workbench/services/backup/test/node/backupFileService.test.ts index 84ae8794ec7..2720754abbb 100644 --- a/src/vs/workbench/services/backup/test/node/backupFileService.test.ts +++ b/src/vs/workbench/services/backup/test/node/backupFileService.test.ts @@ -16,7 +16,7 @@ import pfs = require('vs/base/node/pfs'); import Uri from 'vs/base/common/uri'; import { BackupFileService, BackupFilesModel } from 'vs/workbench/services/backup/node/backupFileService'; import { FileService } from 'vs/workbench/services/files/node/fileService'; -import { createTextBufferFactory } from 'vs/editor/common/model/textModel'; +import { createTextBufferFactory, TextModel } from 'vs/editor/common/model/textModel'; import { TestContextService, TestTextResourceConfigurationService, getRandomTestPath, TestLifecycleService } from 'vs/workbench/test/workbenchTestServices'; import { Workspace, toWorkspaceFolders } from 'vs/platform/workspace/common/workspace'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; @@ -121,6 +121,56 @@ suite('BackupFileService', () => { done(); }); }); + + test('text file (ITextSnapshot)', function (done: () => void) { + const model = TextModel.createFromString('test'); + + service.backupResource(fooFile, model.createSnapshot()).then(() => { + assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'file')).length, 1); + assert.equal(fs.existsSync(fooBackupPath), true); + assert.equal(fs.readFileSync(fooBackupPath), `${fooFile.toString()}\ntest`); + model.dispose(); + done(); + }); + }); + + test('untitled file (ITextSnapshot)', function (done: () => void) { + const model = TextModel.createFromString('test'); + + service.backupResource(untitledFile, model.createSnapshot()).then(() => { + assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'untitled')).length, 1); + assert.equal(fs.existsSync(untitledBackupPath), true); + assert.equal(fs.readFileSync(untitledBackupPath), `${untitledFile.toString()}\ntest`); + model.dispose(); + done(); + }); + }); + + test('text file (large file, ITextSnapshot)', function (done: () => void) { + const largeString = (new Array(100 * 1024)).join('Large String\n'); + const model = TextModel.createFromString(largeString); + + service.backupResource(fooFile, model.createSnapshot()).then(() => { + assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'file')).length, 1); + assert.equal(fs.existsSync(fooBackupPath), true); + assert.equal(fs.readFileSync(fooBackupPath), `${fooFile.toString()}\n${largeString}`); + model.dispose(); + done(); + }); + }); + + test('untitled file (large file, ITextSnapshot)', function (done: () => void) { + const largeString = (new Array(100 * 1024)).join('Large String\n'); + const model = TextModel.createFromString(largeString); + + service.backupResource(untitledFile, model.createSnapshot()).then(() => { + assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'untitled')).length, 1); + assert.equal(fs.existsSync(untitledBackupPath), true); + assert.equal(fs.readFileSync(untitledBackupPath), `${untitledFile.toString()}\n${largeString}`); + model.dispose(); + done(); + }); + }); }); suite('discardResourceBackup', () => { diff --git a/src/vs/workbench/services/files/test/node/fileService.test.ts b/src/vs/workbench/services/files/test/node/fileService.test.ts index 863f43f8721..378b1b91a08 100644 --- a/src/vs/workbench/services/files/test/node/fileService.test.ts +++ b/src/vs/workbench/services/files/test/node/fileService.test.ts @@ -945,26 +945,32 @@ suite('FileService', () => { fs.readFile(resource.fsPath, (error, data) => { assert.equal(encodingLib.detectEncodingByBOMFromBuffer(data, 512), null); + const model = TextModel.createFromString('Hello Bom'); + // Update content: UTF_8 => UTF_8_BOM - _service.updateContent(resource, 'Hello Bom', { encoding: encodingLib.UTF8_with_bom }).done(() => { + _service.updateContent(resource, model.createSnapshot(), { encoding: encodingLib.UTF8_with_bom }).done(() => { fs.readFile(resource.fsPath, (error, data) => { assert.equal(encodingLib.detectEncodingByBOMFromBuffer(data, 512), encodingLib.UTF8); // Update content: PRESERVE BOM when using UTF-8 - _service.updateContent(resource, 'Please stay Bom', { encoding: encodingLib.UTF8 }).done(() => { + model.setValue('Please stay Bom'); + _service.updateContent(resource, model.createSnapshot(), { encoding: encodingLib.UTF8 }).done(() => { fs.readFile(resource.fsPath, (error, data) => { assert.equal(encodingLib.detectEncodingByBOMFromBuffer(data, 512), encodingLib.UTF8); // Update content: REMOVE BOM - _service.updateContent(resource, 'Go away Bom', { encoding: encodingLib.UTF8, overwriteEncoding: true }).done(() => { + model.setValue('Go away Bom'); + _service.updateContent(resource, model.createSnapshot(), { encoding: encodingLib.UTF8, overwriteEncoding: true }).done(() => { fs.readFile(resource.fsPath, (error, data) => { assert.equal(encodingLib.detectEncodingByBOMFromBuffer(data, 512), null); // Update content: BOM comes not back - _service.updateContent(resource, 'Do not come back Bom', { encoding: encodingLib.UTF8 }).done(() => { + model.setValue('Do not come back Bom'); + _service.updateContent(resource, model.createSnapshot(), { encoding: encodingLib.UTF8 }).done(() => { fs.readFile(resource.fsPath, (error, data) => { assert.equal(encodingLib.detectEncodingByBOMFromBuffer(data, 512), null); + model.dispose(); _service.dispose(); done(); }); diff --git a/src/vs/workbench/services/textfile/common/textFileService.ts b/src/vs/workbench/services/textfile/common/textFileService.ts index 6e5bf84bf4a..a1ef38dd364 100644 --- a/src/vs/workbench/services/textfile/common/textFileService.ts +++ b/src/vs/workbench/services/textfile/common/textFileService.ts @@ -241,7 +241,7 @@ export abstract class TextFileService implements ITextFileService { private doBackupAll(dirtyFileModels: ITextFileEditorModel[], untitledResources: URI[]): TPromise { // Handle file resources first - return TPromise.join(dirtyFileModels.map(model => this.backupFileService.backupResource(model.getResource(), model.getValue(), model.getVersionId()))).then(results => { + return TPromise.join(dirtyFileModels.map(model => this.backupFileService.backupResource(model.getResource(), model.createSnapshot(), model.getVersionId()))).then(results => { // Handle untitled resources const untitledModelPromises = untitledResources @@ -250,7 +250,7 @@ export abstract class TextFileService implements ITextFileService { return TPromise.join(untitledModelPromises).then(untitledModels => { const untitledBackupPromises = untitledModels.map(model => { - return this.backupFileService.backupResource(model.getResource(), model.getValue(), model.getVersionId()); + return this.backupFileService.backupResource(model.getResource(), model.createSnapshot(), model.getVersionId()); }); return TPromise.join(untitledBackupPromises).then(() => void 0); diff --git a/src/vs/workbench/services/textfile/common/textfiles.ts b/src/vs/workbench/services/textfile/common/textfiles.ts index 4be8ffb329e..6000c9734e5 100644 --- a/src/vs/workbench/services/textfile/common/textfiles.ts +++ b/src/vs/workbench/services/textfile/common/textfiles.ts @@ -9,7 +9,7 @@ import URI from 'vs/base/common/uri'; import Event from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; import { IEncodingSupport, ConfirmResult } from 'vs/workbench/common/editor'; -import { IBaseStat, IResolveContentOptions } from 'vs/platform/files/common/files'; +import { IBaseStat, IResolveContentOptions, ITextSnapshot } from 'vs/platform/files/common/files'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ITextEditorModel } from 'vs/editor/common/services/resolverService'; import { ITextBufferFactory } from 'vs/editor/common/model'; @@ -202,6 +202,8 @@ export interface ITextFileEditorModel extends ITextEditorModel, IEncodingSupport getValue(): string; + createSnapshot(): ITextSnapshot; + isDirty(): boolean; isResolved(): boolean; diff --git a/src/vs/workbench/test/workbenchTestServices.ts b/src/vs/workbench/test/workbenchTestServices.ts index e6f1b33310f..50f9c781ff8 100644 --- a/src/vs/workbench/test/workbenchTestServices.ts +++ b/src/vs/workbench/test/workbenchTestServices.ts @@ -856,7 +856,7 @@ export class TestBackupFileService implements IBackupFileService { return null; } - public backupResource(resource: URI, content: string): TPromise { + public backupResource(resource: URI, content: string | ITextSnapshot): TPromise { return TPromise.as(void 0); } From fbc94d2a8f61a7bd3a770d2a6d02ce7efe41d7b3 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Sun, 21 Jan 2018 12:43:18 +0100 Subject: [PATCH 032/128] snapshot support for getFirstLineText --- .../workbench/common/editor/textEditorModel.ts | 16 +++++++++++++--- .../common/editor/untitledEditorModel.ts | 9 --------- .../textfile/common/textFileEditorModel.ts | 2 +- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/vs/workbench/common/editor/textEditorModel.ts b/src/vs/workbench/common/editor/textEditorModel.ts index 29550c7197f..8df2f87b499 100644 --- a/src/vs/workbench/common/editor/textEditorModel.ts +++ b/src/vs/workbench/common/editor/textEditorModel.ts @@ -91,7 +91,9 @@ export abstract class BaseTextEditorModel extends EditorModel implements ITextEd return this; } - protected getFirstLineText(value: string | ITextBufferFactory): string { + protected getFirstLineText(value: string | ITextBufferFactory | ITextSnapshot): string { + + // string if (typeof value === 'string') { const firstLineText = value.substr(0, 100); @@ -106,9 +108,17 @@ export abstract class BaseTextEditorModel extends EditorModel implements ITextEd } return firstLineText.substr(0, Math.min(crIndex, lfIndex)); - } else { - return value.getFirstLineText(100); } + + // text buffer factory + const textBufferFactory = value as ITextBufferFactory; + if (typeof textBufferFactory.getFirstLineText === 'function') { + return textBufferFactory.getFirstLineText(100); + } + + // text snapshot + const textSnapshot = value as ITextSnapshot; + return this.getFirstLineText(textSnapshot.read() || ''); } /** diff --git a/src/vs/workbench/common/editor/untitledEditorModel.ts b/src/vs/workbench/common/editor/untitledEditorModel.ts index 9a817653721..c7dcee066bd 100644 --- a/src/vs/workbench/common/editor/untitledEditorModel.ts +++ b/src/vs/workbench/common/editor/untitledEditorModel.ts @@ -10,7 +10,6 @@ import { IEncodingSupport } from 'vs/workbench/common/editor'; import { BaseTextEditorModel } from 'vs/workbench/common/editor/textEditorModel'; import URI from 'vs/base/common/uri'; import { PLAINTEXT_MODE_ID } from 'vs/editor/common/modes/modesRegistry'; -import { EndOfLinePreference } from 'vs/editor/common/model'; import { CONTENT_CHANGE_EVENT_BUFFER_DELAY } from 'vs/platform/files/common/files'; import { IModeService } from 'vs/editor/common/services/modeService'; import { IModelService } from 'vs/editor/common/services/modelService'; @@ -113,14 +112,6 @@ export class UntitledEditorModel extends BaseTextEditorModel implements IEncodin return this.versionId; } - public getValue(): string { - if (this.textEditorModel) { - return this.textEditorModel.getValue(EndOfLinePreference.TextDefined, true /* Preserve BOM */); - } - - return null; - } - public getModeId(): string { if (this.textEditorModel) { return this.textEditorModel.getLanguageIdentifier().language; diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts index beb2ea20d74..88197be4a82 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts @@ -188,7 +188,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil return; } - const firstLineText = this.getFirstLineText(this.textEditorModel.getValue()); + const firstLineText = this.getFirstLineText(this.textEditorModel.createSnapshot()); const mode = this.getOrCreateMode(this.modeService, modeId, firstLineText); this.modelService.setMode(this.textEditorModel, mode); From 0628d026f004687134a54ce1fb55f593bcac1fd4 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Sun, 21 Jan 2018 13:26:15 +0100 Subject: [PATCH 033/128] move BackupSnapshot around --- src/vs/platform/files/common/files.ts | 26 ------------------- .../services/backup/node/backupFileService.ts | 26 +++++++++++++++++-- 2 files changed, 24 insertions(+), 28 deletions(-) diff --git a/src/vs/platform/files/common/files.ts b/src/vs/platform/files/common/files.ts index 9f313e5a55a..0fe4bf63718 100644 --- a/src/vs/platform/files/common/files.ts +++ b/src/vs/platform/files/common/files.ts @@ -481,32 +481,6 @@ export function snapshotToString(snapshot: ITextSnapshot): string { return chunks.join(''); } -/** - * Helper that wraps around a ITextSnapshot and allows to have a - * preamble that the read() method will return first. - */ -export class BufferedTextSnapshot implements ITextSnapshot { - private preambleHandled: boolean; - - constructor(private snapshot: ITextSnapshot, private preamble: string) { - } - - public read(): string { - let value = this.snapshot.read(); - if (!this.preambleHandled) { - this.preambleHandled = true; - - if (typeof value === 'string') { - value = this.preamble + value; - } else { - value = this.preamble; - } - } - - return value; - } -} - /** * Streamable content and meta information of a file. */ diff --git a/src/vs/workbench/services/backup/node/backupFileService.ts b/src/vs/workbench/services/backup/node/backupFileService.ts index 3ef38e18dc5..18c4e2a7b50 100644 --- a/src/vs/workbench/services/backup/node/backupFileService.ts +++ b/src/vs/workbench/services/backup/node/backupFileService.ts @@ -11,7 +11,7 @@ import * as pfs from 'vs/base/node/pfs'; import Uri from 'vs/base/common/uri'; import { ResourceQueue } from 'vs/base/common/async'; import { IBackupFileService, BACKUP_FILE_UPDATE_OPTIONS } from 'vs/workbench/services/backup/common/backup'; -import { IFileService, ITextSnapshot, BufferedTextSnapshot, IFileStat } from 'vs/platform/files/common/files'; +import { IFileService, ITextSnapshot, IFileStat } from 'vs/platform/files/common/files'; import { TPromise } from 'vs/base/common/winjs.base'; import { readToMatchingString } from 'vs/base/node/stream'; import { Range } from 'vs/editor/common/core/range'; @@ -28,6 +28,28 @@ export interface IBackupFilesModel { clear(): void; } +export class BackupSnapshot implements ITextSnapshot { + private preambleHandled: boolean; + + constructor(private snapshot: ITextSnapshot, private preamble: string) { + } + + public read(): string { + let value = this.snapshot.read(); + if (!this.preambleHandled) { + this.preambleHandled = true; + + if (typeof value === 'string') { + value = this.preamble + value; + } else { + value = this.preamble; + } + } + + return value; + } +} + export class BackupFilesModel implements IBackupFilesModel { private cache: { [resource: string]: number /* version ID */ } = Object.create(null); @@ -175,7 +197,7 @@ export class BackupFileService implements IBackupFileService { // Update content with snapshot else { - updateContentPromise = this.fileService.updateContent(backupResource, new BufferedTextSnapshot(content, preamble), BACKUP_FILE_UPDATE_OPTIONS); + updateContentPromise = this.fileService.updateContent(backupResource, new BackupSnapshot(content, preamble), BACKUP_FILE_UPDATE_OPTIONS); } return updateContentPromise.then(() => model.add(backupResource, versionId)); From 9f4dc79a03910415d28145e1798b81b2450dadb0 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Mon, 22 Jan 2018 09:09:02 +0100 Subject: [PATCH 034/128] linux: missing checking for updatse --- src/vs/platform/update/electron-main/updateService.linux.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/vs/platform/update/electron-main/updateService.linux.ts b/src/vs/platform/update/electron-main/updateService.linux.ts index 53cb9a6ae9b..cb36c658585 100644 --- a/src/vs/platform/update/electron-main/updateService.linux.ts +++ b/src/vs/platform/update/electron-main/updateService.linux.ts @@ -44,6 +44,8 @@ export class LinuxUpdateService extends AbstractUpdateService { return; } + this.setState(State.CheckingForUpdates(explicit)); + this.requestService.request({ url: this.url }) .then(asJson) .then(update => { From 45bb5bf3be92f24de8a457a79ed21d23c7c1c943 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Mon, 22 Jan 2018 10:41:54 +0100 Subject: [PATCH 035/128] move inno_updater.exe to windows build directory --- build/gulpfile.vscode.js | 2 ++ build/gulpfile.vscode.win32.js | 2 +- build/win32/code.iss | 4 ++-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/build/gulpfile.vscode.js b/build/gulpfile.vscode.js index a0b70de7f14..90f10ed4240 100644 --- a/build/gulpfile.vscode.js +++ b/build/gulpfile.vscode.js @@ -349,6 +349,8 @@ function packageTask(platform, arch, opts) { result = es.merge(result, gulp.src('resources/win32/VisualElementsManifest.xml', { base: 'resources/win32' }) .pipe(rename(product.nameShort + '.VisualElementsManifest.xml'))); + + result = es.merge(result, gulp.src('build/win32/inno_updater.exe', { base: 'build/win32' })); } else if (platform === 'linux') { result = es.merge(result, gulp.src('resources/linux/bin/code.sh', { base: '.' }) .pipe(replace('@@NAME@@', product.applicationName)) diff --git a/build/gulpfile.vscode.win32.js b/build/gulpfile.vscode.win32.js index 8630284bc1f..a24daa47e9a 100644 --- a/build/gulpfile.vscode.win32.js +++ b/build/gulpfile.vscode.win32.js @@ -77,7 +77,7 @@ gulp.task('vscode-win32-x64-setup', ['clean-vscode-win32-x64-setup'], buildWin32 function archiveWin32Setup(arch) { return cb => { - const args = ['a', '-tzip', zipPath(arch), '.', '-r']; + const args = ['a', '-tzip', zipPath(arch), '.', '-r', '-x!inno_updater.exe']; cp.spawn(_7z, args, { stdio: 'inherit', cwd: buildPath(arch) }) .on('error', cb) diff --git a/build/win32/code.iss b/build/win32/code.iss index fc9b79a33c8..1601f540fa2 100644 --- a/build/win32/code.iss +++ b/build/win32/code.iss @@ -67,8 +67,8 @@ Name: "addtopath"; Description: "{cm:AddToPath}"; GroupDescription: "{cm:Other}" Name: "runcode"; Description: "{cm:RunAfter,{#NameShort}}"; GroupDescription: "{cm:Other}"; Check: WizardSilent [Files] -Source: "*"; DestDir: "{code:GetDestDir}"; Flags: ignoreversion recursesubdirs createallsubdirs -Source: "{#RepoDir}\build\win32\inno_updater.exe"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs +Source: "*"; Excludes: "inno_updater.exe"; DestDir: "{code:GetDestDir}"; Flags: ignoreversion recursesubdirs createallsubdirs +Source: "inno_updater.exe"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs [Icons] Name: "{group}\{#NameLong}"; Filename: "{app}\{#ExeBasename}.exe"; AppUserModelID: "{#AppUserId}" From c18f63a1b7b5daf27a4be4ba09b1bbec1a4aa4b2 Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Mon, 15 Jan 2018 11:00:24 +0100 Subject: [PATCH 036/128] [themes] add 'support.function' to functions (for #41612) --- .../services/themes/electron-browser/colorThemeData.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/services/themes/electron-browser/colorThemeData.ts b/src/vs/workbench/services/themes/electron-browser/colorThemeData.ts index 37e0498d121..fd7ad73cded 100644 --- a/src/vs/workbench/services/themes/electron-browser/colorThemeData.ts +++ b/src/vs/workbench/services/themes/electron-browser/colorThemeData.ts @@ -30,7 +30,7 @@ const tokenGroupToScopesMap: { [setting: string]: string[] } = { keywords: ['keyword', 'keyword.control', 'storage', 'storage.type'], numbers: ['constant.numeric'], types: ['entity.name.type', 'entity.name.class', 'support.type', 'support.class'], - functions: ['entity.name.function'], + functions: ['entity.name.function', 'support.function'], variables: ['variable'] }; From c1ec6c2ff50b2e0d5a4b24abe05ba16748852063 Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Tue, 16 Jan 2018 10:57:27 +0100 Subject: [PATCH 037/128] add download limiter to build/i18n script --- build/lib/i18n.ts | 55 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 52 insertions(+), 3 deletions(-) diff --git a/build/lib/i18n.ts b/build/lib/i18n.ts index db67ffc3812..223b61ce911 100644 --- a/build/lib/i18n.ts +++ b/build/lib/i18n.ts @@ -17,6 +17,8 @@ import * as https from 'https'; var util = require('gulp-util'); var iconv = require('iconv-lite'); +const NUMBER_OF_CONCURRENT_DOWNLOADS = 1; + function log(message: any, ...rest: any[]): void { util.log(util.colors.green('[i18n]'), message, ...rest); } @@ -272,6 +274,49 @@ export class XLF { }; } +export interface ITask { + (): T; +} + +interface ILimitedTaskFactory { + factory: ITask>; + c: (value?: T | Thenable) => void; + e: (error?: any) => void; +} + +export class Limiter { + private runningPromises: number; + private outstandingPromises: ILimitedTaskFactory[]; + + constructor(private maxDegreeOfParalellism: number) { + this.outstandingPromises = []; + this.runningPromises = 0; + } + + queue(factory: ITask>): Promise { + return new Promise((c, e) => { + this.outstandingPromises.push({factory, c, e}); + this.consume(); + }); + } + + private consume(): void { + while (this.outstandingPromises.length && this.runningPromises < this.maxDegreeOfParalellism) { + const iLimitedTask = this.outstandingPromises.shift(); + this.runningPromises++; + + const promise = iLimitedTask.factory(); + promise.then(iLimitedTask.c).catch(iLimitedTask.e); + promise.then(() => this.consumed()).catch(() => this.consumed()); + } + } + + private consumed(): void { + this.runningPromises--; + this.consume(); + } +} + const iso639_3_to_2: Map = { 'chs': 'zh-cn', 'cht': 'zh-tw', @@ -927,9 +972,10 @@ export function pullXlfFiles(projectName: string, apiHostname: string, username: callback(); }); } +const limiter = new Limiter(NUMBER_OF_CONCURRENT_DOWNLOADS); function retrieveResource(language: string, resource: Resource, apiHostname, credentials): Promise { - return new Promise((resolve, reject) => { + return limiter.queue(() => new Promise((resolve, reject) => { const slug = resource.name.replace(/\//g, '_'); const project = resource.project; const iso639 = language.toLowerCase(); @@ -937,6 +983,7 @@ function retrieveResource(language: string, resource: Resource, apiHostname, cre hostname: apiHostname, path: `/api/2/project/${project}/resource/${slug}/translation/${iso639}?file&mode=onlyreviewed`, auth: credentials, + port: 443, method: 'GET' }; @@ -945,16 +992,18 @@ function retrieveResource(language: string, resource: Resource, apiHostname, cre res.on('data', (chunk: Buffer) => xlfBuffer.push(chunk)); res.on('end', () => { if (res.statusCode === 200) { + console.log('success: ' + options.path); resolve(new File({ contents: Buffer.concat(xlfBuffer), path: `${project}/${iso639_2_to_3[language]}/${slug}.xlf` })); } reject(`${slug} in ${project} returned no data. Response code: ${res.statusCode}.`); }); }); request.on('error', (err) => { - reject(`Failed to query resource ${slug} with the following error: ${err}`); + reject(`Failed to query resource ${slug} with the following error: ${err}. ${options.path}`); }); request.end(); - }); + console.log('started: ' + options.path); + })); } export function prepareJsonFiles(): ThroughStream { From 19bab787f4e7e970cabc7340008d56eadc129f61 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Mon, 22 Jan 2018 10:47:00 +0100 Subject: [PATCH 038/128] move inno_updater into app when building setup --- build/gulpfile.vscode.js | 2 -- build/gulpfile.vscode.win32.js | 16 +++++++++++++--- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/build/gulpfile.vscode.js b/build/gulpfile.vscode.js index 90f10ed4240..a0b70de7f14 100644 --- a/build/gulpfile.vscode.js +++ b/build/gulpfile.vscode.js @@ -349,8 +349,6 @@ function packageTask(platform, arch, opts) { result = es.merge(result, gulp.src('resources/win32/VisualElementsManifest.xml', { base: 'resources/win32' }) .pipe(rename(product.nameShort + '.VisualElementsManifest.xml'))); - - result = es.merge(result, gulp.src('build/win32/inno_updater.exe', { base: 'build/win32' })); } else if (platform === 'linux') { result = es.merge(result, gulp.src('resources/linux/bin/code.sh', { base: '.' }) .pipe(replace('@@NAME@@', product.applicationName)) diff --git a/build/gulpfile.vscode.win32.js b/build/gulpfile.vscode.win32.js index a24daa47e9a..b8d0be61116 100644 --- a/build/gulpfile.vscode.win32.js +++ b/build/gulpfile.vscode.win32.js @@ -9,10 +9,12 @@ const gulp = require('gulp'); const path = require('path'); const assert = require('assert'); const cp = require('child_process'); +const es = require('event-stream'); const _7z = require('7zip')['7z']; const util = require('./lib/util'); const pkg = require('../package.json'); const product = require('../product.json'); +const vfs = require('vinyl-fs'); const repoPath = path.dirname(__dirname); const buildPath = arch => path.join(path.dirname(repoPath), `VSCode-win32-${arch}`); @@ -38,8 +40,8 @@ function packageInnoSetup(iss, options, cb) { .on('exit', () => cb(null)); } -function buildWin32Setup(arch) { - return cb => { +function _buildWin32Setup(arch) { + return es.through(null, function () { const ia32AppId = product.win32AppId; const x64AppId = product.win32x64AppId; @@ -65,7 +67,15 @@ function buildWin32Setup(arch) { OutputDir: setupDir(arch) }; - packageInnoSetup(issPath, { definitions }, cb); + packageInnoSetup(issPath, { definitions }, err => err ? this.emit('error', err) : this.emit('end')); + }); +} + +function buildWin32Setup(arch) { + return () => { + return gulp.src('build/win32/inno_updater.exe', { base: 'build/win32' }) + .pipe(vfs.dest(buildPath)) + .pipe(_buildWin32Setup(arch)); }; } From 7b55cebb00783d170a0e3612033b2255ec467e31 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Mon, 22 Jan 2018 11:10:04 +0100 Subject: [PATCH 039/128] show progress bar for update in progress --- .../parts/activitybar/activitybarPart.ts | 6 ++--- .../parts/compositebar/compositeBarActions.ts | 23 +++++++++++++++---- .../parts/update/electron-browser/update.ts | 4 +++- 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts index 627be4bee34..122d34883f6 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts @@ -93,10 +93,10 @@ export class ActivitybarPart extends Part { return this.compositeBar.showActivity(viewletOrActionId, badge, clazz, priority); } - return this.showGlobalActivity(viewletOrActionId, badge); + return this.showGlobalActivity(viewletOrActionId, badge, clazz); } - private showGlobalActivity(globalActivityId: string, badge: IBadge): IDisposable { + private showGlobalActivity(globalActivityId: string, badge: IBadge, clazz?: string): IDisposable { if (!badge) { throw illegalArgument('badge'); } @@ -106,7 +106,7 @@ export class ActivitybarPart extends Part { throw illegalArgument('globalActivityId'); } - action.setBadge(badge); + action.setBadge(badge, clazz); return toDisposable(() => action.setBadge(undefined)); } diff --git a/src/vs/workbench/browser/parts/compositebar/compositeBarActions.ts b/src/vs/workbench/browser/parts/compositebar/compositeBarActions.ts index 6c4f3102f84..61b5c8d5697 100644 --- a/src/vs/workbench/browser/parts/compositebar/compositeBarActions.ts +++ b/src/vs/workbench/browser/parts/compositebar/compositeBarActions.ts @@ -12,7 +12,7 @@ import * as dom from 'vs/base/browser/dom'; import { Builder, $ } from 'vs/base/browser/builder'; import { BaseActionItem, IBaseActionItemOptions, Separator } from 'vs/base/browser/ui/actionbar/actionbar'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { dispose } from 'vs/base/common/lifecycle'; +import { dispose, IDisposable, empty, toDisposable } from 'vs/base/common/lifecycle'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IThemeService, ITheme } from 'vs/platform/theme/common/themeService'; import { TextBadge, NumberBadge, IBadge, IconBadge, ProgressBadge } from 'vs/workbench/services/activity/common/activity'; @@ -53,6 +53,7 @@ export interface ICompositeBar { export class ActivityAction extends Action { private badge: IBadge; + private clazz: string | undefined; private _onDidChangeBadge = new Emitter(); constructor(private _activity: IActivity) { @@ -85,8 +86,13 @@ export class ActivityAction extends Action { return this.badge; } - public setBadge(badge: IBadge): void { + public getClass(): string | undefined { + return this.clazz; + } + + public setBadge(badge: IBadge, clazz?: string): void { this.badge = badge; + this.clazz = clazz; this._onDidChangeBadge.fire(this); } } @@ -110,6 +116,7 @@ export class ActivityActionItem extends BaseActionItem { protected options: IActivityActionItemOptions; private $badgeContent: Builder; + private badgeDisposable: IDisposable = empty; private mouseUpTimeout: number; constructor( @@ -199,7 +206,10 @@ export class ActivityActionItem extends BaseActionItem { this.updateStyles(); } - protected updateBadge(badge: IBadge): void { + protected updateBadge(badge: IBadge, clazz?: string): void { + this.badgeDisposable.dispose(); + this.badgeDisposable = empty; + this.$badgeContent.empty(); this.$badge.hide(); @@ -234,6 +244,11 @@ export class ActivityActionItem extends BaseActionItem { else if (badge instanceof ProgressBadge) { this.$badge.show(); } + + if (clazz) { + this.$badge.addClass(clazz); + this.badgeDisposable = toDisposable(() => this.$badge.removeClass(clazz)); + } } // Title @@ -259,7 +274,7 @@ export class ActivityActionItem extends BaseActionItem { private handleBadgeChangeEvenet(): void { const action = this.getAction(); if (action instanceof ActivityAction) { - this.updateBadge(action.getBadge()); + this.updateBadge(action.getBadge(), action.getClass()); } } diff --git a/src/vs/workbench/parts/update/electron-browser/update.ts b/src/vs/workbench/parts/update/electron-browser/update.ts index 2c705a72974..380a3c068eb 100644 --- a/src/vs/workbench/parts/update/electron-browser/update.ts +++ b/src/vs/workbench/parts/update/electron-browser/update.ts @@ -356,17 +356,19 @@ export class UpdateContribution implements IGlobalActivity { } let badge: IBadge | undefined = undefined; + let clazz: string | undefined; if (state.type === StateType.AvailableForDownload || state.type === StateType.Downloaded || state.type === StateType.Ready) { badge = new NumberBadge(1, () => nls.localize('updateIsReady', "New {0} update available.", product.nameShort)); } else if (state.type === StateType.CheckingForUpdates || state.type === StateType.Downloading || state.type === StateType.Updating) { badge = new ProgressBadge(() => nls.localize('updateIsReady', "New {0} update available.", product.nameShort)); + clazz = 'progress-badge'; } this.badgeDisposable.dispose(); if (badge) { - this.badgeDisposable = this.activityService.showActivity(this.id, badge); + this.badgeDisposable = this.activityService.showActivity(this.id, badge, clazz); } this.state = state; From 2d45445592a502dbbcd2bc47ff05e8a055ac2c52 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Mon, 22 Jan 2018 11:14:04 +0100 Subject: [PATCH 040/128] Fix #41781 --- .../parts/extensions/electron-browser/extensionsViews.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensionsViews.ts b/src/vs/workbench/parts/extensions/electron-browser/extensionsViews.ts index 3c7d35378b0..df49559b538 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/extensionsViews.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/extensionsViews.ts @@ -234,7 +234,7 @@ export class ExtensionsListView extends ViewsViewletPanel { const languageTag = languageName ? ` tag:"${languageName}"` : ''; // Construct a rich query - return `tag:"__ext_${ext}" ${keywords.map(tag => `tag:"${tag}"`).join(' ')}${languageTag}`; + return `tag:"__ext_.${ext}" ${keywords.map(tag => `tag:"${tag}"`).join(' ')}${languageTag}`; }); if (names.length) { From cdb7246a2df5daa3abe6d8b23ec00555da415e14 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Mon, 22 Jan 2018 11:16:23 +0100 Subject: [PATCH 041/128] add log statements to update service --- .../update/electron-main/abstractUpdateService.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/vs/platform/update/electron-main/abstractUpdateService.ts b/src/vs/platform/update/electron-main/abstractUpdateService.ts index 97055d435d1..fcc47b9f7a6 100644 --- a/src/vs/platform/update/electron-main/abstractUpdateService.ts +++ b/src/vs/platform/update/electron-main/abstractUpdateService.ts @@ -34,6 +34,7 @@ export abstract class AbstractUpdateService implements IUpdateService { } protected setState(state: State): void { + this.logService.info('update#setState', state.type); this._state = state; this._onStateChange.fire(state); } @@ -45,20 +46,24 @@ export abstract class AbstractUpdateService implements IUpdateService { @ILogService protected logService: ILogService ) { if (this.environmentService.disableUpdates) { + this.logService.info('update#ctor - updates are disabled'); return; } if (!product.updateUrl || !product.commit) { + this.logService.info('update#ctor - updates are disabled'); return; } const quality = this.getProductQuality(); if (!quality) { + this.logService.info('update#ctor - updates are disabled'); return; } if (!this.setUpdateFeedUrl(quality)) { + this.logService.info('update#ctor - updates are disabled'); return; } @@ -89,6 +94,8 @@ export abstract class AbstractUpdateService implements IUpdateService { } checkForUpdates(explicit = false): TPromise { + this.logService.trace('update#checkForUpdates, state = ', this.state.type); + if (this.state.type !== StateType.Idle) { return TPromise.as(null); } @@ -97,6 +104,8 @@ export abstract class AbstractUpdateService implements IUpdateService { } downloadUpdate(): TPromise { + this.logService.trace('update#downloadUpdate, state = ', this.state.type); + if (this.state.type !== StateType.AvailableForDownload) { return TPromise.as(null); } @@ -109,6 +118,8 @@ export abstract class AbstractUpdateService implements IUpdateService { } applyUpdate(): TPromise { + this.logService.trace('update#applyUpdate, state = ', this.state.type); + if (this.state.type !== StateType.Ready) { return TPromise.as(null); } @@ -121,6 +132,8 @@ export abstract class AbstractUpdateService implements IUpdateService { } quitAndInstall(): TPromise { + this.logService.trace('update#quitAndInstall, state = ', this.state.type); + if (this.state.type !== StateType.Ready) { return TPromise.as(null); } From 0be4f4ec9dc339f1cd79b5dacc41ea5d2e16bc74 Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Mon, 22 Jan 2018 11:12:42 +0100 Subject: [PATCH 042/128] [i18n] push compiles js file --- build/lib/i18n.js | 41 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/build/lib/i18n.js b/build/lib/i18n.js index 1be9709523f..1358c7dfdda 100644 --- a/build/lib/i18n.js +++ b/build/lib/i18n.js @@ -14,6 +14,7 @@ var glob = require("glob"); var https = require("https"); var util = require('gulp-util'); var iconv = require('iconv-lite'); +var NUMBER_OF_CONCURRENT_DOWNLOADS = 1; function log(message) { var rest = []; for (var _i = 1; _i < arguments.length; _i++) { @@ -207,6 +208,36 @@ var XLF = /** @class */ (function () { return XLF; }()); exports.XLF = XLF; +var Limiter = /** @class */ (function () { + function Limiter(maxDegreeOfParalellism) { + this.maxDegreeOfParalellism = maxDegreeOfParalellism; + this.outstandingPromises = []; + this.runningPromises = 0; + } + Limiter.prototype.queue = function (factory) { + var _this = this; + return new Promise(function (c, e) { + _this.outstandingPromises.push({ factory: factory, c: c, e: e }); + _this.consume(); + }); + }; + Limiter.prototype.consume = function () { + var _this = this; + while (this.outstandingPromises.length && this.runningPromises < this.maxDegreeOfParalellism) { + var iLimitedTask = this.outstandingPromises.shift(); + this.runningPromises++; + var promise = iLimitedTask.factory(); + promise.then(iLimitedTask.c).catch(iLimitedTask.e); + promise.then(function () { return _this.consumed(); }).catch(function () { return _this.consumed(); }); + } + }; + Limiter.prototype.consumed = function () { + this.runningPromises--; + this.consume(); + }; + return Limiter; +}()); +exports.Limiter = Limiter; var iso639_3_to_2 = { 'chs': 'zh-cn', 'cht': 'zh-tw', @@ -826,8 +857,9 @@ function pullXlfFiles(projectName, apiHostname, username, password, languages, r }); } exports.pullXlfFiles = pullXlfFiles; +var limiter = new Limiter(NUMBER_OF_CONCURRENT_DOWNLOADS); function retrieveResource(language, resource, apiHostname, credentials) { - return new Promise(function (resolve, reject) { + return limiter.queue(function () { return new Promise(function (resolve, reject) { var slug = resource.name.replace(/\//g, '_'); var project = resource.project; var iso639 = language.toLowerCase(); @@ -835,6 +867,7 @@ function retrieveResource(language, resource, apiHostname, credentials) { hostname: apiHostname, path: "/api/2/project/" + project + "/resource/" + slug + "/translation/" + iso639 + "?file&mode=onlyreviewed", auth: credentials, + port: 443, method: 'GET' }; var request = https.request(options, function (res) { @@ -842,16 +875,18 @@ function retrieveResource(language, resource, apiHostname, credentials) { res.on('data', function (chunk) { return xlfBuffer.push(chunk); }); res.on('end', function () { if (res.statusCode === 200) { + console.log('success: ' + options.path); resolve(new File({ contents: Buffer.concat(xlfBuffer), path: project + "/" + iso639_2_to_3[language] + "/" + slug + ".xlf" })); } reject(slug + " in " + project + " returned no data. Response code: " + res.statusCode + "."); }); }); request.on('error', function (err) { - reject("Failed to query resource " + slug + " with the following error: " + err); + reject("Failed to query resource " + slug + " with the following error: " + err + ". " + options.path); }); request.end(); - }); + console.log('started: ' + options.path); + }); }); } function prepareJsonFiles() { var parsePromises = []; From 8500a2ec5338d8f29842a966d22450bc14a04e1d Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Mon, 22 Jan 2018 11:26:26 +0100 Subject: [PATCH 043/128] Keywords and Storage.types have the same color in Dark theme for Go. Fixes #41822 --- extensions/go/test/colorize-fixtures/test.go | 1 + .../go/test/colorize-results/test_go.json | 121 ++++++++++++++++++ .../theme-defaults/themes/dark_plus.json | 5 + .../theme-defaults/themes/light_plus.json | 5 + 4 files changed, 132 insertions(+) diff --git a/extensions/go/test/colorize-fixtures/test.go b/extensions/go/test/colorize-fixtures/test.go index ef1d22a3bff..ade235742c6 100644 --- a/extensions/go/test/colorize-fixtures/test.go +++ b/extensions/go/test/colorize-fixtures/test.go @@ -8,6 +8,7 @@ import ( func main() { dnsName := "test-vm-from-go" storageAccount := "mystorageaccount" + c := make(chan int) client, err := management.ClientFromPublishSettingsFile("path/to/downloaded.publishsettings", "") if err != nil { diff --git a/extensions/go/test/colorize-results/test_go.json b/extensions/go/test/colorize-results/test_go.json index 0a876071e60..6916ec49cd2 100644 --- a/extensions/go/test/colorize-results/test_go.json +++ b/extensions/go/test/colorize-results/test_go.json @@ -428,6 +428,127 @@ "hc_black": "default: #FFFFFF" } }, + { + "c": "c", + "t": "source.go variable.other.assignment.go", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE" + } + }, + { + "c": " ", + "t": "source.go", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": ":=", + "t": "source.go keyword.operator.assignment.go", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4" + } + }, + { + "c": " ", + "t": "source.go", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "make", + "t": "source.go support.function.builtin.go", + "r": { + "dark_plus": "support.function: #DCDCAA", + "light_plus": "support.function: #795E26", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.function: #DCDCAA" + } + }, + { + "c": "(", + "t": "source.go punctuation.definition.begin.bracket.round.go", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "chan", + "t": "source.go keyword.channel.go", + "r": { + "dark_plus": "keyword: #569CD6", + "light_plus": "keyword: #0000FF", + "dark_vs": "keyword: #569CD6", + "light_vs": "keyword: #0000FF", + "hc_black": "keyword: #569CD6" + } + }, + { + "c": " ", + "t": "source.go", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "int", + "t": "source.go storage.type.numeric.go", + "r": { + "dark_plus": "storage.type.numeric.go: #4EC9B0", + "light_plus": "storage.type.numeric.go: #267F99", + "dark_vs": "storage.type: #569CD6", + "light_vs": "storage.type: #0000FF", + "hc_black": "storage.type: #569CD6" + } + }, + { + "c": ")", + "t": "source.go punctuation.definition.end.bracket.round.go", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": " ", + "t": "source.go", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, { "c": "client", "t": "source.go variable.other.assignment.go", diff --git a/extensions/theme-defaults/themes/dark_plus.json b/extensions/theme-defaults/themes/dark_plus.json index fe868c9a375..9767fa79c74 100644 --- a/extensions/theme-defaults/themes/dark_plus.json +++ b/extensions/theme-defaults/themes/dark_plus.json @@ -22,6 +22,11 @@ "support.type", "entity.name.type", "entity.name.class", + "storage.type.numeric.go", + "storage.type.byte.go", + "storage.type.boolean.go", + "storage.type.string.go", + "storage.type.uintptr.go", "storage.type.cs", "storage.type.generic.cs", "storage.type.modifier.cs", diff --git a/extensions/theme-defaults/themes/light_plus.json b/extensions/theme-defaults/themes/light_plus.json index 0ab71013c2f..a4b6b642a9a 100644 --- a/extensions/theme-defaults/themes/light_plus.json +++ b/extensions/theme-defaults/themes/light_plus.json @@ -22,6 +22,11 @@ "support.type", "entity.name.type", "entity.name.class", + "storage.type.numeric.go", + "storage.type.byte.go", + "storage.type.boolean.go", + "storage.type.string.go", + "storage.type.uintptr.go", "storage.type.cs", "storage.type.generic.cs", "storage.type.modifier.cs", From 236b928218e11ce551e52607d7d8b731087e0893 Mon Sep 17 00:00:00 2001 From: isidor Date: Mon, 22 Jan 2018 11:27:24 +0100 Subject: [PATCH 044/128] add a todo --- src/vs/workbench/parts/debug/electron-browser/debugCommands.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/workbench/parts/debug/electron-browser/debugCommands.ts b/src/vs/workbench/parts/debug/electron-browser/debugCommands.ts index 37e840a1666..af425b59ffb 100644 --- a/src/vs/workbench/parts/debug/electron-browser/debugCommands.ts +++ b/src/vs/workbench/parts/debug/electron-browser/debugCommands.ts @@ -22,6 +22,7 @@ import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; export function registerCommands(): void { + // TODO@Isidor remove in february KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'debug.logToDebugConsole', weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), From a76e372236251409135935909ff512b7f4d36ae9 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Mon, 22 Jan 2018 11:39:15 +0100 Subject: [PATCH 045/128] Remove duplicated code --- .../browser/viewParts/lines/viewLine.ts | 53 +++++++------------ 1 file changed, 19 insertions(+), 34 deletions(-) diff --git a/src/vs/editor/browser/viewParts/lines/viewLine.ts b/src/vs/editor/browser/viewParts/lines/viewLine.ts index 5f83e8df989..1b17027275a 100644 --- a/src/vs/editor/browser/viewParts/lines/viewLine.ts +++ b/src/vs/editor/browser/viewParts/lines/viewLine.ts @@ -278,8 +278,27 @@ export class ViewLine implements IVisibleLine { } public getVisibleRangesForRange(startColumn: number, endColumn: number, context: DomReadingContext): HorizontalRange[] { + startColumn = startColumn | 0; // @perf + endColumn = endColumn | 0; // @perf + startColumn = Math.min(this._renderedViewLine.input.lineContent.length + 1, Math.max(1, startColumn)); endColumn = Math.min(this._renderedViewLine.input.lineContent.length + 1, Math.max(1, endColumn)); + + const stopRenderingLineAfter = this._renderedViewLine.input.stopRenderingLineAfter | 0; // @perf + + if (stopRenderingLineAfter !== -1 && startColumn > stopRenderingLineAfter && endColumn > stopRenderingLineAfter) { + // This range is obviously not visible + return null; + } + + if (stopRenderingLineAfter !== -1 && startColumn > stopRenderingLineAfter) { + startColumn = stopRenderingLineAfter; + } + + if (stopRenderingLineAfter !== -1 && endColumn > stopRenderingLineAfter) { + endColumn = stopRenderingLineAfter; + } + return this._renderedViewLine.getVisibleRangesForRange(startColumn, endColumn, context); } @@ -325,23 +344,6 @@ class FastRenderedViewLine implements IRenderedViewLine { } public getVisibleRangesForRange(startColumn: number, endColumn: number, context: DomReadingContext): HorizontalRange[] { - startColumn = startColumn | 0; // @perf - endColumn = endColumn | 0; // @perf - const stopRenderingLineAfter = this.input.stopRenderingLineAfter | 0; // @perf - - if (stopRenderingLineAfter !== -1 && startColumn > stopRenderingLineAfter && endColumn > stopRenderingLineAfter) { - // This range is obviously not visible - return null; - } - - if (stopRenderingLineAfter !== -1 && startColumn > stopRenderingLineAfter) { - startColumn = stopRenderingLineAfter; - } - - if (stopRenderingLineAfter !== -1 && endColumn > stopRenderingLineAfter) { - endColumn = stopRenderingLineAfter; - } - const startPosition = this._getCharPosition(startColumn); const endPosition = this._getCharPosition(endColumn); return [new HorizontalRange(startPosition, endPosition - startPosition)]; @@ -432,23 +434,6 @@ class RenderedViewLine implements IRenderedViewLine { * Visible ranges for a model range */ public getVisibleRangesForRange(startColumn: number, endColumn: number, context: DomReadingContext): HorizontalRange[] { - startColumn = startColumn | 0; // @perf - endColumn = endColumn | 0; // @perf - const stopRenderingLineAfter = this.input.stopRenderingLineAfter | 0; // @perf - - if (stopRenderingLineAfter !== -1 && startColumn > stopRenderingLineAfter && endColumn > stopRenderingLineAfter) { - // This range is obviously not visible - return null; - } - - if (stopRenderingLineAfter !== -1 && startColumn > stopRenderingLineAfter) { - startColumn = stopRenderingLineAfter; - } - - if (stopRenderingLineAfter !== -1 && endColumn > stopRenderingLineAfter) { - endColumn = stopRenderingLineAfter; - } - if (this._pixelOffsetCache !== null) { // the text is LTR let startOffset = this._readPixelOffset(startColumn, context); From a1b0cc77c9fc30b9ff764c118b851569f7c9ec31 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Mon, 22 Jan 2018 11:53:52 +0100 Subject: [PATCH 046/128] Fixes #33971: Stricter check for using FastRenderedViewLine --- src/vs/editor/browser/viewParts/lines/viewLine.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/vs/editor/browser/viewParts/lines/viewLine.ts b/src/vs/editor/browser/viewParts/lines/viewLine.ts index 1b17027275a..7a9761d20d8 100644 --- a/src/vs/editor/browser/viewParts/lines/viewLine.ts +++ b/src/vs/editor/browser/viewParts/lines/viewLine.ts @@ -228,9 +228,12 @@ export class ViewLine implements IVisibleLine { isRegularASCII = strings.isBasicASCII(lineData.content); } - if (isRegularASCII && lineData.content.length < 1000) { + if (isRegularASCII && lineData.content.length < 1000 && renderLineInput.lineTokens.getCount() < 100) { // Browser rounding errors have been observed in Chrome and IE, so using the fast // view line only for short lines. Please test before removing the length check... + // --- + // Another rounding error has been observed on Linux in VSCode, where width + // rounding errors add up to an observable large number... renderedViewLine = new FastRenderedViewLine( this._renderedViewLine ? this._renderedViewLine.domNode : null, renderLineInput, From 11e05b6a3939a63980f8a0a5d495e6c4fb2c1172 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Mon, 22 Jan 2018 12:07:47 +0100 Subject: [PATCH 047/128] fix bad build script --- build/gulpfile.vscode.win32.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/gulpfile.vscode.win32.js b/build/gulpfile.vscode.win32.js index b8d0be61116..a7ecdf66f5a 100644 --- a/build/gulpfile.vscode.win32.js +++ b/build/gulpfile.vscode.win32.js @@ -74,7 +74,7 @@ function _buildWin32Setup(arch) { function buildWin32Setup(arch) { return () => { return gulp.src('build/win32/inno_updater.exe', { base: 'build/win32' }) - .pipe(vfs.dest(buildPath)) + .pipe(vfs.dest(buildPath(arch))) .pipe(_buildWin32Setup(arch)); }; } From d73725c38419bc19d25d75824e8a688de71d68a0 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Mon, 22 Jan 2018 12:23:50 +0100 Subject: [PATCH 048/128] Fixes #41925: ERR Illegal value 0 for `lineNumber` --- .../workbench/api/electron-browser/mainThreadSaveParticipant.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/api/electron-browser/mainThreadSaveParticipant.ts b/src/vs/workbench/api/electron-browser/mainThreadSaveParticipant.ts index 07a61e0f3b8..ad20179ae9b 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadSaveParticipant.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadSaveParticipant.ts @@ -145,7 +145,7 @@ export class TrimFinalNewLinesParticipant implements ISaveParticipantParticipant const lineCount = model.getLineCount(); // Do not insert new line if file does not end with new line - if (!lineCount) { + if (lineCount === 1) { return; } From 12d45a9bcee51469c425141edf70a408eddb4807 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Mon, 22 Jan 2018 15:39:39 +0100 Subject: [PATCH 049/128] Revert "Fix #41781" This reverts commit 2d45445592a502dbbcd2bc47ff05e8a055ac2c52. --- .../parts/extensions/electron-browser/extensionsViews.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensionsViews.ts b/src/vs/workbench/parts/extensions/electron-browser/extensionsViews.ts index df49559b538..3c7d35378b0 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/extensionsViews.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/extensionsViews.ts @@ -234,7 +234,7 @@ export class ExtensionsListView extends ViewsViewletPanel { const languageTag = languageName ? ` tag:"${languageName}"` : ''; // Construct a rich query - return `tag:"__ext_.${ext}" ${keywords.map(tag => `tag:"${tag}"`).join(' ')}${languageTag}`; + return `tag:"__ext_${ext}" ${keywords.map(tag => `tag:"${tag}"`).join(' ')}${languageTag}`; }); if (names.length) { From 060896c9019819c1f7ebe7fcf99041a8cc579ba5 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 22 Jan 2018 06:40:03 -0800 Subject: [PATCH 050/128] Announce terminal to screen reader Part of #8339 --- package.json | 2 +- yarn.lock | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 3312dc66b2d..5720232afe8 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "vscode-debugprotocol": "1.25.0", "vscode-ripgrep": "^0.7.1-patch.0", "vscode-textmate": "^3.2.0", - "vscode-xterm": "3.1.0-beta5", + "vscode-xterm": "3.1.0-beta6", "yauzl": "2.8.0" }, "devDependencies": { diff --git a/yarn.lock b/yarn.lock index 8c4b2723702..50fc2a3bae6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5808,9 +5808,9 @@ vscode-textmate@^3.2.0: fast-plist "^0.1.2" oniguruma "^6.0.1" -vscode-xterm@3.1.0-beta5: - version "3.1.0-beta5" - resolved "https://registry.yarnpkg.com/vscode-xterm/-/vscode-xterm-3.1.0-beta5.tgz#b63c48cacda9c2546f50de550fef973a24df284c" +vscode-xterm@3.1.0-beta6: + version "3.1.0-beta6" + resolved "https://registry.yarnpkg.com/vscode-xterm/-/vscode-xterm-3.1.0-beta6.tgz#0ff44249ac141e9f6dbcf0b7628d0b8d87e69abf" vso-node-api@^6.1.2-preview: version "6.1.2-preview" From 4846088f22202df577c6b84b5c29745abefff730 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Mon, 22 Jan 2018 15:47:18 +0100 Subject: [PATCH 051/128] fix inno updater --- build/gulpfile.vscode.win32.js | 25 +++++++++++++------------ build/tfs/win32/1_build.ps1 | 4 ++++ 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/build/gulpfile.vscode.win32.js b/build/gulpfile.vscode.win32.js index a7ecdf66f5a..15459f7dae8 100644 --- a/build/gulpfile.vscode.win32.js +++ b/build/gulpfile.vscode.win32.js @@ -9,7 +9,6 @@ const gulp = require('gulp'); const path = require('path'); const assert = require('assert'); const cp = require('child_process'); -const es = require('event-stream'); const _7z = require('7zip')['7z']; const util = require('./lib/util'); const pkg = require('../package.json'); @@ -40,8 +39,8 @@ function packageInnoSetup(iss, options, cb) { .on('exit', () => cb(null)); } -function _buildWin32Setup(arch) { - return es.through(null, function () { +function buildWin32Setup(arch) { + return cb => { const ia32AppId = product.win32AppId; const x64AppId = product.win32x64AppId; @@ -67,15 +66,7 @@ function _buildWin32Setup(arch) { OutputDir: setupDir(arch) }; - packageInnoSetup(issPath, { definitions }, err => err ? this.emit('error', err) : this.emit('end')); - }); -} - -function buildWin32Setup(arch) { - return () => { - return gulp.src('build/win32/inno_updater.exe', { base: 'build/win32' }) - .pipe(vfs.dest(buildPath(arch))) - .pipe(_buildWin32Setup(arch)); + packageInnoSetup(issPath, { definitions }, cb); }; } @@ -100,3 +91,13 @@ gulp.task('vscode-win32-ia32-archive', ['clean-vscode-win32-ia32-archive'], arch gulp.task('clean-vscode-win32-x64-archive', util.rimraf(zipDir('x64'))); gulp.task('vscode-win32-x64-archive', ['clean-vscode-win32-x64-archive'], archiveWin32Setup('x64')); + +function copyInnoUpdater(arch) { + return () => { + return gulp.src('build/win32/inno_updater.exe', { base: 'build/win32' }) + .pipe(vfs.dest(buildPath(arch))); + }; +} + +gulp.task('vscode-win32-ia32-copy-inno-updater', copyInnoUpdater('ia32')); +gulp.task('vscode-win32-x64-copy-inno-updater', copyInnoUpdater('x64')); \ No newline at end of file diff --git a/build/tfs/win32/1_build.ps1 b/build/tfs/win32/1_build.ps1 index bc6ade13de3..0fcb56c1b9e 100644 --- a/build/tfs/win32/1_build.ps1 +++ b/build/tfs/win32/1_build.ps1 @@ -45,6 +45,10 @@ step "Build minified" { exec { & npm run gulp -- "vscode-win32-$global:arch-min" } } +step "Copy Inno updater" { + exec { & npm run gulp -- "vscode-win32-$global:arch-copy-inno-updater" } +} + # step "Create loader snapshot" { # exec { & node build\lib\snapshotLoader.js --arch=$global:arch } # } From de220368be1cf08ece9d5a9bd8bb858f1e921994 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Mon, 22 Jan 2018 15:45:46 +0100 Subject: [PATCH 052/128] Fix #41781 --- .../parts/extensions/electron-browser/extensionsViews.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensionsViews.ts b/src/vs/workbench/parts/extensions/electron-browser/extensionsViews.ts index 3c7d35378b0..03694331729 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/extensionsViews.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/extensionsViews.ts @@ -234,7 +234,7 @@ export class ExtensionsListView extends ViewsViewletPanel { const languageTag = languageName ? ` tag:"${languageName}"` : ''; // Construct a rich query - return `tag:"__ext_${ext}" ${keywords.map(tag => `tag:"${tag}"`).join(' ')}${languageTag}`; + return `tag:"__ext_${ext}" tag:"__ext_.${ext}" ${keywords.map(tag => `tag:"${tag}"`).join(' ')}${languageTag}`; }); if (names.length) { From a99da1688f9451c013b63ea22d8c9578f81eaa86 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 22 Jan 2018 07:01:35 -0800 Subject: [PATCH 053/128] Add screen reader keywords to navigation mode command --- .../parts/terminal/electron-browser/terminal.contribution.ts | 3 +-- .../parts/terminal/electron-browser/terminalActions.ts | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminal.contribution.ts b/src/vs/workbench/parts/terminal/electron-browser/terminal.contribution.ts index 6c596e10385..98e2a2357b8 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/terminal.contribution.ts +++ b/src/vs/workbench/parts/terminal/electron-browser/terminal.contribution.ts @@ -394,9 +394,8 @@ actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(DeleteWordRightT }, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Delete Word Right', category); actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(EnterNavigationModeTerminalAction, EnterNavigationModeTerminalAction.ID, EnterNavigationModeTerminalAction.LABEL, { primary: KeyMod.CtrlCmd | KeyCode.KEY_N -}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Enter Navigation Mode', category); +}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Enter Screen Reader Navigation Mode', category); terminalCommands.setup(); registerColors(); - diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminalActions.ts b/src/vs/workbench/parts/terminal/electron-browser/terminalActions.ts index 4e9752cafa3..3c1b515e62b 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/terminalActions.ts +++ b/src/vs/workbench/parts/terminal/electron-browser/terminalActions.ts @@ -199,7 +199,7 @@ export class DeleteWordRightTerminalAction extends Action { export class EnterNavigationModeTerminalAction extends Action { public static readonly ID = 'workbench.action.terminal.enterLineNavigationMode'; - public static readonly LABEL = nls.localize('workbench.action.terminal.enterLineNavigationMode', "Enter Line Navigation Mode"); + public static readonly LABEL = nls.localize('workbench.action.terminal.enterLineNavigationMode', "Enter Screen Reader Navigation Mode"); constructor( id: string, label: string, From 56b067cfdf8d24d93a3261da639bc44106d90fed Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 22 Jan 2018 16:10:00 +0100 Subject: [PATCH 054/128] adopt more snapshots --- src/vs/editor/common/model/textModel.ts | 11 +++++++++++ .../parts/files/electron-browser/saveErrorHandler.ts | 5 +++-- .../services/textfile/common/textFileService.ts | 7 +++++-- .../textfile/electron-browser/textFileService.ts | 4 +++- src/vs/workbench/test/workbenchTestServices.ts | 5 +++-- 5 files changed, 25 insertions(+), 7 deletions(-) diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index 90e95ac7bb7..27c58c24d2e 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -82,6 +82,17 @@ export function createTextBufferFactoryFromStream(stream: IStringStream): TPromi }); } +export function createTextBufferFactoryFromSnapshot(snapshot: ITextSnapshot): model.ITextBufferFactory { + let builder = createTextBufferBuilder(); + + let chunk: string; + while (typeof (chunk = snapshot.read()) === 'string') { + builder.acceptChunk(chunk); + } + + return builder.finish(); +} + export function createTextBuffer(value: string | model.ITextBufferFactory, defaultEOL: model.DefaultEndOfLine): model.ITextBuffer { const factory = (typeof value === 'string' ? createTextBufferFactory(value) : value); return factory.create(defaultEOL); diff --git a/src/vs/workbench/parts/files/electron-browser/saveErrorHandler.ts b/src/vs/workbench/parts/files/electron-browser/saveErrorHandler.ts index 379a2a53d81..3a0864e2ea7 100644 --- a/src/vs/workbench/parts/files/electron-browser/saveErrorHandler.ts +++ b/src/vs/workbench/parts/files/electron-browser/saveErrorHandler.ts @@ -31,6 +31,7 @@ import { FileEditorInput } from 'vs/workbench/parts/files/common/editors/fileEdi import { IModelService } from 'vs/editor/common/services/modelService'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { SAVE_FILE_COMMAND_ID, REVERT_FILE_COMMAND_ID, SAVE_FILE_AS_COMMAND_ID, SAVE_FILE_AS_LABEL } from 'vs/workbench/parts/files/electron-browser/fileCommands'; +import { createTextBufferFactoryFromSnapshot } from 'vs/editor/common/model/textModel'; export const CONFLICT_RESOLUTION_CONTEXT = 'saveConflictResolutionContext'; export const CONFLICT_RESOLUTION_SCHEME = 'conflictResolution'; @@ -262,7 +263,7 @@ export const acceptLocalChangesCommand = (accessor: ServicesAccessor, resource: resolverService.createModelReference(resource).then(reference => { const model = reference.object as ITextFileEditorModel; - const localModelValue = model.getValue(); + const localModelSnapshot = model.createSnapshot(); clearPendingResolveSaveConflictMessages(); // hide any previously shown message about how to use these actions @@ -270,7 +271,7 @@ export const acceptLocalChangesCommand = (accessor: ServicesAccessor, resource: return model.revert().then(() => { // Restore user value (without loosing undo stack) - modelService.updateModel(model.textEditorModel, localModelValue); + modelService.updateModel(model.textEditorModel, createTextBufferFactoryFromSnapshot(localModelSnapshot)); // Trigger save return model.save().then(() => { diff --git a/src/vs/workbench/services/textfile/common/textFileService.ts b/src/vs/workbench/services/textfile/common/textFileService.ts index a1ef38dd364..42f1d466370 100644 --- a/src/vs/workbench/services/textfile/common/textFileService.ts +++ b/src/vs/workbench/services/textfile/common/textFileService.ts @@ -32,6 +32,8 @@ import { Schemas } from 'vs/base/common/network'; import { IHistoryService } from 'vs/workbench/services/history/common/history'; import { IRevertOptions } from 'vs/platform/editor/common/editor'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { createTextBufferFactoryFromSnapshot } from 'vs/editor/common/model/textModel'; +import { IModelService } from 'vs/editor/common/services/modelService'; export interface IBackupResult { didBackup: boolean; @@ -73,7 +75,8 @@ export abstract class TextFileService implements ITextFileService { private backupFileService: IBackupFileService, private windowsService: IWindowsService, private historyService: IHistoryService, - contextKeyService: IContextKeyService + contextKeyService: IContextKeyService, + private modelService: IModelService ) { this.toUnbind = []; @@ -615,7 +618,7 @@ export abstract class TextFileService implements ITextFileService { // take over encoding and model value from source model targetModel.updatePreferredEncoding(sourceModel.getEncoding()); - targetModel.textEditorModel.setValue(sourceModel.getValue()); + this.modelService.updateModel(targetModel.textEditorModel, createTextBufferFactoryFromSnapshot(sourceModel.createSnapshot())); // save model return targetModel.save(options); diff --git a/src/vs/workbench/services/textfile/electron-browser/textFileService.ts b/src/vs/workbench/services/textfile/electron-browser/textFileService.ts index ecf4229c83b..1dcfc4c6334 100644 --- a/src/vs/workbench/services/textfile/electron-browser/textFileService.ts +++ b/src/vs/workbench/services/textfile/electron-browser/textFileService.ts @@ -30,6 +30,7 @@ import { IWindowsService, IWindowService } from 'vs/platform/windows/common/wind import { IHistoryService } from 'vs/workbench/services/history/common/history'; import { mnemonicButtonLabel } from 'vs/base/common/labels'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IModelService } from 'vs/editor/common/services/modelService'; export class TextFileService extends AbstractTextFileService { @@ -41,6 +42,7 @@ export class TextFileService extends AbstractTextFileService { @IInstantiationService instantiationService: IInstantiationService, @IConfigurationService configurationService: IConfigurationService, @IModeService private modeService: IModeService, + @IModelService modelService: IModelService, @IWindowService private windowService: IWindowService, @IEnvironmentService environmentService: IEnvironmentService, @IMessageService messageService: IMessageService, @@ -49,7 +51,7 @@ export class TextFileService extends AbstractTextFileService { @IHistoryService historyService: IHistoryService, @IContextKeyService contextKeyService: IContextKeyService ) { - super(lifecycleService, contextService, configurationService, fileService, untitledEditorService, instantiationService, messageService, environmentService, backupFileService, windowsService, historyService, contextKeyService); + super(lifecycleService, contextService, configurationService, fileService, untitledEditorService, instantiationService, messageService, environmentService, backupFileService, windowsService, historyService, contextKeyService, modelService); } public resolveTextContent(resource: URI, options?: IResolveContentOptions): TPromise { diff --git a/src/vs/workbench/test/workbenchTestServices.ts b/src/vs/workbench/test/workbenchTestServices.ts index 50f9c781ff8..e5db572cc91 100644 --- a/src/vs/workbench/test/workbenchTestServices.ts +++ b/src/vs/workbench/test/workbenchTestServices.ts @@ -180,9 +180,10 @@ export class TestTextFileService extends TextFileService { @IBackupFileService backupFileService: IBackupFileService, @IWindowsService windowsService: IWindowsService, @IHistoryService historyService: IHistoryService, - @IContextKeyService contextKeyService: IContextKeyService + @IContextKeyService contextKeyService: IContextKeyService, + @IModelService modelService: IModelService ) { - super(lifecycleService, contextService, configurationService, fileService, untitledEditorService, instantiationService, messageService, TestEnvironmentService, backupFileService, windowsService, historyService, contextKeyService); + super(lifecycleService, contextService, configurationService, fileService, untitledEditorService, instantiationService, messageService, TestEnvironmentService, backupFileService, windowsService, historyService, contextKeyService, modelService); } public setPromptPath(path: string): void { From 73d60bd4c8308f018c4bd761d38702a8f0e91c3a Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Mon, 22 Jan 2018 17:56:18 +0100 Subject: [PATCH 055/128] Fixes #41503: Add fast path for searching for \n --- src/vs/editor/common/model/textModelSearch.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/vs/editor/common/model/textModelSearch.ts b/src/vs/editor/common/model/textModelSearch.ts index aa969b5334a..2f1526e618e 100644 --- a/src/vs/editor/common/model/textModelSearch.ts +++ b/src/vs/editor/common/model/textModelSearch.ts @@ -136,6 +136,23 @@ export class TextModelSearch { } if (searchData.regex.multiline) { + if (searchData.regex.source === '\\n') { + // Fast path for searching for EOL + let result: FindMatch[] = [], resultLen = 0; + for (let lineNumber = 1, lineCount = model.getLineCount(); lineNumber < lineCount; lineNumber++) { + const range = new Range(lineNumber, model.getLineMaxColumn(lineNumber), lineNumber + 1, 1); + if (captureMatches) { + result[resultLen++] = new FindMatch(range, null); + } else { + result[resultLen++] = new FindMatch(range, ['\n']); + } + + if (resultLen >= limitResultCount) { + break; + } + } + return result; + } return this._doFindMatchesMultiline(model, searchRange, new Searcher(searchData.wordSeparators, searchData.regex), captureMatches, limitResultCount); } return this._doFindMatchesLineByLine(model, searchRange, searchData, captureMatches, limitResultCount); From b25c1947c6bcb43298914ee32d11e82e178978d5 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Mon, 22 Jan 2018 17:58:51 +0100 Subject: [PATCH 056/128] #41752 Update extension point --- .../configuration-editing/src/extension.ts | 10 +-- .../contrib/languagePackExtensions.ts | 16 +++-- .../common/extensionEnablementService.ts | 2 +- .../common/extensionManagement.ts | 9 +-- .../common/extensionEnablementService.test.ts | 2 +- .../browser/localizationsExtensionPoint.ts | 69 +++++++++++++++++++ src/vs/workbench/workbench.main.ts | 3 + 7 files changed, 93 insertions(+), 18 deletions(-) create mode 100644 src/vs/workbench/api/browser/localizationsExtensionPoint.ts diff --git a/extensions/configuration-editing/src/extension.ts b/extensions/configuration-editing/src/extension.ts index aacb21ff777..a081f0ad448 100644 --- a/extensions/configuration-editing/src/extension.ts +++ b/extensions/configuration-editing/src/extension.ts @@ -87,11 +87,11 @@ function registerLocaleCompletionsInLanguageDocument(): vscode.Disposable { function provideContributedLocalesProposals(range: vscode.Range): vscode.ProviderResult { const contributedLocales: string[] = []; for (const extension of vscode.extensions.all) { - if (extension.packageJSON && extension.packageJSON['contributes'] && extension.packageJSON['contributes']['locales'] && extension.packageJSON['contributes']['locales'].length) { - const locales: { locale: string }[] = extension.packageJSON['contributes']['locales']; - for (const locale of locales) { - if (contributedLocales.indexOf(locale.locale) === -1) { - contributedLocales.push(locale.locale); + if (extension.packageJSON && extension.packageJSON['contributes'] && extension.packageJSON['contributes']['localizations'] && extension.packageJSON['contributes']['localizations'].length) { + const localizations: { languageId: string }[] = extension.packageJSON['contributes']['localizations']; + for (const localization of localizations) { + if (contributedLocales.indexOf(localization.languageId) === -1) { + contributedLocales.push(localization.languageId); } } } diff --git a/src/vs/code/electron-browser/sharedProcess/contrib/languagePackExtensions.ts b/src/vs/code/electron-browser/sharedProcess/contrib/languagePackExtensions.ts index 1add53f4777..64fd8395763 100644 --- a/src/vs/code/electron-browser/sharedProcess/contrib/languagePackExtensions.ts +++ b/src/vs/code/electron-browser/sharedProcess/contrib/languagePackExtensions.ts @@ -16,7 +16,7 @@ import { ILogService } from 'vs/platform/log/common/log'; interface ILanguageSource { extensionIdentifier: IExtensionIdentifier; version: string; - path: string; + translations: string; } export class LanguagePackExtensions extends Disposable { @@ -52,7 +52,7 @@ export class LanguagePackExtensions extends Disposable { } private onDidInstallExtension(extension: ILocalExtension): void { - if (extension && extension.manifest && extension.manifest.contributes && extension.manifest.contributes.locales && extension.manifest.contributes.locales.length) { + if (extension && extension.manifest && extension.manifest.contributes && extension.manifest.contributes.localizations && extension.manifest.contributes.localizations.length) { this.logService.debug('Adding language packs from the extension', extension.identifier.id); this.withLanguagePacks(languagePacks => { this.removeLanguagePacksFromExtensions(languagePacks, { id: getGalleryExtensionIdFromLocal(extension), uuid: extension.identifier.uuid }); @@ -68,12 +68,14 @@ export class LanguagePackExtensions extends Disposable { private addLanguagePacksFromExtensions(languagePacks: { [language: string]: ILanguageSource[] }, ...extensions: ILocalExtension[]): void { for (const extension of extensions) { - if (extension && extension.manifest && extension.manifest.contributes && extension.manifest.contributes.locales && extension.manifest.contributes.locales.length) { + 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 localeContribution of extension.manifest.contributes.locales) { - const languageSources = languagePacks[localeContribution.locale] || []; - languageSources.splice(0, 0, { extensionIdentifier, path: join(extension.path, localeContribution.path), version: extension.manifest.version }); - languagePacks[localeContribution.locale] = languageSources; + for (const localizationContribution of extension.manifest.contributes.localizations) { + if (localizationContribution.languagId && localizationContribution.translations) { + const languageSources = languagePacks[localizationContribution.languagId] || []; + languageSources.splice(0, 0, { extensionIdentifier, translations: join(extension.path, localizationContribution.translations), version: extension.manifest.version }); + languagePacks[localizationContribution.languagId] = languageSources; + } } } } diff --git a/src/vs/platform/extensionManagement/common/extensionEnablementService.ts b/src/vs/platform/extensionManagement/common/extensionEnablementService.ts index 990894ef862..8e4c9917668 100644 --- a/src/vs/platform/extensionManagement/common/extensionEnablementService.ts +++ b/src/vs/platform/extensionManagement/common/extensionEnablementService.ts @@ -78,7 +78,7 @@ export class ExtensionEnablementService implements IExtensionEnablementService { } canChangeEnablement(extension: ILocalExtension): boolean { - return !this.environmentService.disableExtensions && !(extension.manifest && extension.manifest.contributes && extension.manifest.contributes.locales && extension.manifest.contributes.locales.length); + return !this.environmentService.disableExtensions && !(extension.manifest && extension.manifest.contributes && extension.manifest.contributes.localizations && extension.manifest.contributes.localizations.length); } setEnablement(arg: ILocalExtension | IExtensionIdentifier, newState: EnablementState): TPromise { diff --git a/src/vs/platform/extensionManagement/common/extensionManagement.ts b/src/vs/platform/extensionManagement/common/extensionManagement.ts index 28f19aa05df..9789db5e966 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -85,9 +85,10 @@ export interface IColor { defaults: { light: string, dark: string, highContrast: string }; } -export interface ILocale { - locale: string; - path: string; +export interface ILocalization { + languagId: string; + languageName?: string; + translations: string; } export interface IExtensionContributions { @@ -104,7 +105,7 @@ export interface IExtensionContributions { iconThemes?: ITheme[]; views?: { [location: string]: IView[] }; colors?: IColor[]; - locales?: ILocale[]; + localizations?: ILocalization[]; } export interface IExtensionManifest { diff --git a/src/vs/platform/extensionManagement/test/common/extensionEnablementService.test.ts b/src/vs/platform/extensionManagement/test/common/extensionEnablementService.test.ts index 4ccb6691229..efdb210e498 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', { locales: [{ locale: 'gr', path: 'somepath' }] })), false); + assert.equal(testObject.canChangeEnablement(aLocalExtension('pub.a', { localizations: [{ languagId: 'gr', translations: 'somepath' }] })), false); }); }); diff --git a/src/vs/workbench/api/browser/localizationsExtensionPoint.ts b/src/vs/workbench/api/browser/localizationsExtensionPoint.ts new file mode 100644 index 00000000000..dcead032694 --- /dev/null +++ b/src/vs/workbench/api/browser/localizationsExtensionPoint.ts @@ -0,0 +1,69 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import { localize } from 'vs/nls'; +import { IJSONSchema } from 'vs/base/common/jsonSchema'; +import { ExtensionMessageCollector, ExtensionsRegistry } from 'vs/platform/extensions/common/extensionsRegistry'; + +namespace schema { + + // --localizations contribution point + + export interface ILocalizationDescriptor { + languageId: string; + languageName: string; + translations: string; + } + + export function validateLocalizationDescriptors(localizationDescriptors: ILocalizationDescriptor[], collector: ExtensionMessageCollector): boolean { + if (!Array.isArray(localizationDescriptors)) { + collector.error(localize('requirearray', "localizations must be an array")); + return false; + } + + for (let descriptor of localizationDescriptors) { + if (typeof descriptor.languageId !== 'string') { + collector.error(localize('requirestring', "property `{0}` is mandatory and must be of type `string`", 'languageId')); + return false; + } + if (typeof descriptor.languageName !== 'string') { + collector.error(localize('optstring', "property `{0}` can be omitted or must be of type `string`", 'languageName')); + return false; + } + if (descriptor.translations && typeof descriptor.translations !== 'string') { + collector.error(localize('requirestring', "property `{0}` is mandatory and must be of type `string`", 'translations')); + return false; + } + } + + return true; + } + + export const localizationsContribution: IJSONSchema = { + description: localize('vscode.extension.contributes.localizations', "Contributes localizations to the editor"), + type: 'array', + items: { + type: 'object', + properties: { + id: { + description: localize('vscode.extension.contributes.localizations.languageId', 'Id of the language into which the display strings are translated.'), + type: 'string' + }, + name: { + description: localize('vscode.extension.contributes.localizations.languageName', 'Name of the language into which the display strings are translated.'), + type: 'string' + }, + translations: { + description: localize('vscode.extension.contributes.localizations.translations', 'A relative path to the folder containing all translation files for the contributed language.'), + type: 'string' + } + } + } + }; +} + +ExtensionsRegistry.registerExtensionPoint('localizations', [], schema.localizationsContribution) + .setHandler((extensions) => extensions.forEach(extension => schema.validateLocalizationDescriptors(extension.value, extension.collector))); \ No newline at end of file diff --git a/src/vs/workbench/workbench.main.ts b/src/vs/workbench/workbench.main.ts index cc5fa97e31a..2966cea4559 100644 --- a/src/vs/workbench/workbench.main.ts +++ b/src/vs/workbench/workbench.main.ts @@ -21,6 +21,9 @@ import 'vs/platform/actions/electron-browser/menusExtensionPoint'; // Views import 'vs/workbench/api/browser/viewsExtensionPoint'; +// Localizations +import 'vs/workbench/api/browser/localizationsExtensionPoint'; + // Workbench import 'vs/workbench/browser/actions/toggleActivityBarVisibility'; import 'vs/workbench/browser/actions/toggleStatusbarVisibility'; From d1c2e526f041d07e2746df02556b489c08312042 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Mon, 22 Jan 2018 18:02:14 +0100 Subject: [PATCH 057/128] #41752 Add default value for translations folder --- src/vs/workbench/api/browser/localizationsExtensionPoint.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/api/browser/localizationsExtensionPoint.ts b/src/vs/workbench/api/browser/localizationsExtensionPoint.ts index dcead032694..cd92ed0e803 100644 --- a/src/vs/workbench/api/browser/localizationsExtensionPoint.ts +++ b/src/vs/workbench/api/browser/localizationsExtensionPoint.ts @@ -58,7 +58,8 @@ namespace schema { }, translations: { description: localize('vscode.extension.contributes.localizations.translations', 'A relative path to the folder containing all translation files for the contributed language.'), - type: 'string' + type: 'string', + default: 'translations' } } } From c6d150ebb49217241b7ba629c2fbc7f0c446fd97 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 22 Jan 2018 18:06:35 +0100 Subject: [PATCH 058/128] remove textEditorModel.getValue --- .../common/editor/textEditorModel.ts | 14 +------------ .../test/browser/fileEditorTracker.test.ts | 6 +++--- .../parts/search/browser/replaceService.ts | 3 ++- .../editor/test/browser/editorService.test.ts | 3 ++- .../services/textfile/common/textfiles.ts | 2 -- .../textfile/test/textFileEditorModel.test.ts | 4 ++-- .../test/textModelResolverService.test.ts | 3 ++- .../common/editor/resourceEditorInput.test.ts | 3 ++- .../test/common/editor/untitledEditor.test.ts | 3 ++- .../api/mainThreadSaveParticipant.test.ts | 21 ++++++++++--------- 10 files changed, 27 insertions(+), 35 deletions(-) diff --git a/src/vs/workbench/common/editor/textEditorModel.ts b/src/vs/workbench/common/editor/textEditorModel.ts index 8df2f87b499..8c10065dfcc 100644 --- a/src/vs/workbench/common/editor/textEditorModel.ts +++ b/src/vs/workbench/common/editor/textEditorModel.ts @@ -5,7 +5,7 @@ 'use strict'; import { TPromise } from 'vs/base/common/winjs.base'; -import { EndOfLinePreference, ITextModel, ITextBufferFactory } from 'vs/editor/common/model'; +import { ITextModel, ITextBufferFactory } from 'vs/editor/common/model'; import { IMode } from 'vs/editor/common/modes'; import { EditorModel } from 'vs/workbench/common/editor'; import URI from 'vs/base/common/uri'; @@ -141,18 +141,6 @@ export abstract class BaseTextEditorModel extends EditorModel implements ITextEd this.modelService.updateModel(this.textEditorModel, newValue); } - /** - * Returns the textual value of this editor model or null if it has not yet been created. - */ - public getValue(): string { - const model = this.textEditorModel; - if (model) { - return model.getValue(EndOfLinePreference.TextDefined, true /* Preserve BOM */); - } - - return null; - } - public createSnapshot(): ITextSnapshot { const model = this.textEditorModel; if (model) { diff --git a/src/vs/workbench/parts/files/test/browser/fileEditorTracker.test.ts b/src/vs/workbench/parts/files/test/browser/fileEditorTracker.test.ts index 4b9590f037f..a76038659ec 100644 --- a/src/vs/workbench/parts/files/test/browser/fileEditorTracker.test.ts +++ b/src/vs/workbench/parts/files/test/browser/fileEditorTracker.test.ts @@ -16,7 +16,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService'; import { EditorStacksModel } from 'vs/workbench/common/editor/editorStacksModel'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; -import { FileOperation, FileOperationEvent, FileChangesEvent, FileChangeType, IFileService } from 'vs/platform/files/common/files'; +import { FileOperation, FileOperationEvent, FileChangesEvent, FileChangeType, IFileService, snapshotToString } from 'vs/platform/files/common/files'; import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; import { once } from 'vs/base/common/event'; @@ -191,14 +191,14 @@ suite('Files - FileEditorTracker', () => { accessor.textFileService.models.loadOrCreate(resource).then((model: TextFileEditorModel) => { model.textEditorModel.setValue('Super Good'); - assert.equal(model.getValue(), 'Super Good'); + assert.equal(snapshotToString(model.createSnapshot()), 'Super Good'); model.save().then(() => { // change event (watcher) accessor.fileService.fireFileChanges(new FileChangesEvent([{ resource, type: FileChangeType.UPDATED }])); - assert.equal(model.getValue(), 'Hello Html'); + assert.equal(snapshotToString(model.createSnapshot()), 'Hello Html'); tracker.dispose(); diff --git a/src/vs/workbench/parts/search/browser/replaceService.ts b/src/vs/workbench/parts/search/browser/replaceService.ts index c93549e85dd..fca4c6b1a36 100644 --- a/src/vs/workbench/parts/search/browser/replaceService.ts +++ b/src/vs/workbench/parts/search/browser/replaceService.ts @@ -24,6 +24,7 @@ import { ScrollType } from 'vs/editor/common/editorCommon'; import { ITextModel } from 'vs/editor/common/model'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IFileService } from 'vs/platform/files/common/files'; +import { createTextBufferFactoryFromSnapshot } from 'vs/editor/common/model/textModel'; const REPLACE_PREVIEW = 'replacePreview'; @@ -70,7 +71,7 @@ class ReplacePreviewModel extends Disposable { ref = this._register(ref); const sourceModel = ref.object.textEditorModel; const sourceModelModeId = sourceModel.getLanguageIdentifier().language; - const replacePreviewModel = this.modelService.createModel(sourceModel.getValue(), this.modeService.getOrCreateMode(sourceModelModeId), replacePreviewUri); + const replacePreviewModel = this.modelService.createModel(createTextBufferFactoryFromSnapshot(sourceModel.createSnapshot()), this.modeService.getOrCreateMode(sourceModelModeId), replacePreviewUri); this._register(fileMatch.onChange(modelChange => this.update(sourceModel, replacePreviewModel, fileMatch, modelChange))); this._register(this.searchWorkbenchService.searchModel.onReplaceTermChanged(() => this.update(sourceModel, replacePreviewModel, fileMatch))); this._register(fileMatch.onDispose(() => replacePreviewModel.dispose())); // TODO@Sandeep we should not dispose a model directly but rather the reference (depends on https://github.com/Microsoft/vscode/issues/17073) diff --git a/src/vs/workbench/services/editor/test/browser/editorService.test.ts b/src/vs/workbench/services/editor/test/browser/editorService.test.ts index 196825b2f11..e5fd893ab73 100644 --- a/src/vs/workbench/services/editor/test/browser/editorService.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorService.test.ts @@ -19,6 +19,7 @@ import { UntitledEditorInput } from 'vs/workbench/common/editor/untitledEditorIn import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; import { ICloseEditorsFilter } from 'vs/workbench/browser/parts/editor/editorPart'; +import { snapshotToString } from 'vs/platform/files/common/files'; let activeEditor: BaseEditor = { getSelection: function () { @@ -163,7 +164,7 @@ suite('WorkbenchEditorService', () => { const untitledInput = openedEditorInput as UntitledEditorInput; untitledInput.resolve().then(model => { - assert.equal(model.getValue(), 'Hello Untitled'); + assert.equal(snapshotToString(model.createSnapshot()), 'Hello Untitled'); }); }); diff --git a/src/vs/workbench/services/textfile/common/textfiles.ts b/src/vs/workbench/services/textfile/common/textfiles.ts index 6000c9734e5..eacc46d1970 100644 --- a/src/vs/workbench/services/textfile/common/textfiles.ts +++ b/src/vs/workbench/services/textfile/common/textfiles.ts @@ -200,8 +200,6 @@ export interface ITextFileEditorModel extends ITextEditorModel, IEncodingSupport revert(soft?: boolean): TPromise; - getValue(): string; - createSnapshot(): ITextSnapshot; isDirty(): boolean; diff --git a/src/vs/workbench/services/textfile/test/textFileEditorModel.test.ts b/src/vs/workbench/services/textfile/test/textFileEditorModel.test.ts index 5e36065442d..d747bb23b3f 100644 --- a/src/vs/workbench/services/textfile/test/textFileEditorModel.test.ts +++ b/src/vs/workbench/services/textfile/test/textFileEditorModel.test.ts @@ -14,7 +14,7 @@ import { ITextFileService, ModelState, StateChange } from 'vs/workbench/services import { workbenchInstantiationService, TestTextFileService, createFileInput, TestFileService } from 'vs/workbench/test/workbenchTestServices'; import { onError, toResource } from 'vs/base/test/common/utils'; import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager'; -import { FileOperationResult, FileOperationError, IFileService } from 'vs/platform/files/common/files'; +import { FileOperationResult, FileOperationError, IFileService, snapshotToString } from 'vs/platform/files/common/files'; import { IModelService } from 'vs/editor/common/services/modelService'; class ServiceAccessor { @@ -284,7 +284,7 @@ suite('Files - TextFileEditorModel', () => { model.onDidStateChange(e => { if (e === StateChange.SAVED) { - assert.equal(model.getValue(), 'bar'); + assert.equal(snapshotToString(model.createSnapshot()), 'bar'); assert.ok(!model.isDirty()); eventCounter++; } diff --git a/src/vs/workbench/services/textmodelResolver/test/textModelResolverService.test.ts b/src/vs/workbench/services/textmodelResolver/test/textModelResolverService.test.ts index f5bde377342..3c904f154a4 100644 --- a/src/vs/workbench/services/textmodelResolver/test/textModelResolverService.test.ts +++ b/src/vs/workbench/services/textmodelResolver/test/textModelResolverService.test.ts @@ -22,6 +22,7 @@ import { ITextFileService } from 'vs/workbench/services/textfile/common/textfile import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager'; import { once } from 'vs/base/common/event'; +import { snapshotToString } from 'vs/platform/files/common/files'; class ServiceAccessor { constructor( @@ -73,7 +74,7 @@ suite('Workbench - TextModelResolverService', () => { input.resolve().then(model => { assert.ok(model); - assert.equal((model as ResourceEditorModel).getValue(), 'Hello Test'); + assert.equal(snapshotToString((model as ResourceEditorModel).createSnapshot()), 'Hello Test'); let disposed = false; once(model.onDispose)(() => { diff --git a/src/vs/workbench/test/common/editor/resourceEditorInput.test.ts b/src/vs/workbench/test/common/editor/resourceEditorInput.test.ts index 85c63eb3e1b..bed57cb0b05 100644 --- a/src/vs/workbench/test/common/editor/resourceEditorInput.test.ts +++ b/src/vs/workbench/test/common/editor/resourceEditorInput.test.ts @@ -13,6 +13,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { workbenchInstantiationService } from 'vs/workbench/test/workbenchTestServices'; import { IModelService } from 'vs/editor/common/services/modelService'; import { IModeService } from 'vs/editor/common/services/modeService'; +import { snapshotToString } from 'vs/platform/files/common/files'; class ServiceAccessor { constructor( @@ -39,7 +40,7 @@ suite('Workbench - ResourceEditorInput', () => { return input.resolve().then((model: ResourceEditorModel) => { assert.ok(model); - assert.equal(model.getValue(), 'function test() {}'); + assert.equal(snapshotToString(model.createSnapshot()), 'function test() {}'); }); }); }); \ No newline at end of file diff --git a/src/vs/workbench/test/common/editor/untitledEditor.test.ts b/src/vs/workbench/test/common/editor/untitledEditor.test.ts index b7b8a04dd4a..8edf3bb24aa 100644 --- a/src/vs/workbench/test/common/editor/untitledEditor.test.ts +++ b/src/vs/workbench/test/common/editor/untitledEditor.test.ts @@ -17,6 +17,7 @@ import { UntitledEditorModel } from 'vs/workbench/common/editor/untitledEditorMo import { IModeService } from 'vs/editor/common/services/modeService'; import { ModeServiceImpl } from 'vs/editor/common/services/modeServiceImpl'; import { UntitledEditorInput } from 'vs/workbench/common/editor/untitledEditorInput'; +import { snapshotToString } from 'vs/platform/files/common/files'; export class TestUntitledEditorService extends UntitledEditorService { @@ -142,7 +143,7 @@ suite('Workbench - Untitled Editor', () => { assert.ok(!model1.isDirty()); return service.loadOrCreate({ initialValue: 'Hello World' }).then(model2 => { - assert.equal(model2.getValue(), 'Hello World'); + assert.equal(snapshotToString(model2.createSnapshot()), 'Hello World'); const input = service.createOrGet(); diff --git a/src/vs/workbench/test/electron-browser/api/mainThreadSaveParticipant.test.ts b/src/vs/workbench/test/electron-browser/api/mainThreadSaveParticipant.test.ts index d6f3681d331..d83503b679e 100644 --- a/src/vs/workbench/test/electron-browser/api/mainThreadSaveParticipant.test.ts +++ b/src/vs/workbench/test/electron-browser/api/mainThreadSaveParticipant.test.ts @@ -17,6 +17,7 @@ import { Selection } from 'vs/editor/common/core/selection'; import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; import { ITextFileService, SaveReason } from 'vs/workbench/services/textfile/common/textfiles'; import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager'; +import { snapshotToString } from 'vs/platform/files/common/files'; class ServiceAccessor { constructor( @ITextFileService public textFileService: TestTextFileService, @IModelService public modelService: IModelService) { @@ -51,25 +52,25 @@ suite('MainThreadSaveParticipant', function () { let lineContent = ''; model.textEditorModel.setValue(lineContent); participant.participate(model, { reason: SaveReason.EXPLICIT }); - assert.equal(model.getValue(), lineContent); + assert.equal(snapshotToString(model.createSnapshot()), lineContent); // No new line if last line already empty lineContent = `Hello New Line${model.textEditorModel.getEOL()}`; model.textEditorModel.setValue(lineContent); participant.participate(model, { reason: SaveReason.EXPLICIT }); - assert.equal(model.getValue(), lineContent); + assert.equal(snapshotToString(model.createSnapshot()), lineContent); // New empty line added (single line) lineContent = 'Hello New Line'; model.textEditorModel.setValue(lineContent); participant.participate(model, { reason: SaveReason.EXPLICIT }); - assert.equal(model.getValue(), `${lineContent}${model.textEditorModel.getEOL()}`); + assert.equal(snapshotToString(model.createSnapshot()), `${lineContent}${model.textEditorModel.getEOL()}`); // New empty line added (multi line) lineContent = `Hello New Line${model.textEditorModel.getEOL()}Hello New Line${model.textEditorModel.getEOL()}Hello New Line`; model.textEditorModel.setValue(lineContent); participant.participate(model, { reason: SaveReason.EXPLICIT }); - assert.equal(model.getValue(), `${lineContent}${model.textEditorModel.getEOL()}`); + assert.equal(snapshotToString(model.createSnapshot()), `${lineContent}${model.textEditorModel.getEOL()}`); done(); }); @@ -91,25 +92,25 @@ suite('MainThreadSaveParticipant', function () { let lineContent = `${textContent}`; model.textEditorModel.setValue(lineContent); participant.participate(model, { reason: SaveReason.EXPLICIT }); - assert.equal(model.getValue(), lineContent); + assert.equal(snapshotToString(model.createSnapshot()), lineContent); // No new line removal if last line is single new line lineContent = `${textContent}${eol}`; model.textEditorModel.setValue(lineContent); participant.participate(model, { reason: SaveReason.EXPLICIT }); - assert.equal(model.getValue(), lineContent); + assert.equal(snapshotToString(model.createSnapshot()), lineContent); // Remove new line (single line with two new lines) lineContent = `${textContent}${eol}${eol}`; model.textEditorModel.setValue(lineContent); participant.participate(model, { reason: SaveReason.EXPLICIT }); - assert.equal(model.getValue(), `${textContent}${eol}`); + assert.equal(snapshotToString(model.createSnapshot()), `${textContent}${eol}`); // Remove new lines (multiple lines with multiple new lines) lineContent = `${textContent}${eol}${textContent}${eol}${eol}${eol}`; model.textEditorModel.setValue(lineContent); participant.participate(model, { reason: SaveReason.EXPLICIT }); - assert.equal(model.getValue(), `${textContent}${eol}${textContent}${eol}`); + assert.equal(snapshotToString(model.createSnapshot()), `${textContent}${eol}${textContent}${eol}`); done(); }); @@ -134,11 +135,11 @@ suite('MainThreadSaveParticipant', function () { model.textEditorModel.pushEditOperations([new Selection(1, 14, 1, 14)], textEdits, () => { return [new Selection(1, 15, 1, 15)]; }); // undo model.textEditorModel.undo(); - assert.equal(model.getValue(), `${textContent}`); + assert.equal(snapshotToString(model.createSnapshot()), `${textContent}`); // trim final new lines should not mess the undo stack participant.participate(model, { reason: SaveReason.EXPLICIT }); model.textEditorModel.redo(); - assert.equal(model.getValue(), `${textContent}.`); + assert.equal(snapshotToString(model.createSnapshot()), `${textContent}.`); done(); }); }); From e7e73637a98012a20c318764c8cb98f2bf4b53d8 Mon Sep 17 00:00:00 2001 From: Zhongliang Wang Date: Tue, 23 Jan 2018 01:41:44 +0800 Subject: [PATCH 059/128] Add workbench.fontAliasing.auto option (#41895) * Add workbench.fontAliasing.auto option * Remove monaco-font-aliasing-default class * Remove monaco-font-aliasing-auto class for non-retina displays to behave as `default` --- .../electron-browser/main.contribution.ts | 7 ++++--- .../electron-browser/media/workbench.css | 14 +++++++++++++ .../workbench/electron-browser/workbench.ts | 21 +++++++++++++------ 3 files changed, 33 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/electron-browser/main.contribution.ts b/src/vs/workbench/electron-browser/main.contribution.ts index 6bcbf87b95d..d72fb4e04e6 100644 --- a/src/vs/workbench/electron-browser/main.contribution.ts +++ b/src/vs/workbench/electron-browser/main.contribution.ts @@ -256,14 +256,15 @@ configurationRegistry.registerConfiguration({ }, 'workbench.fontAliasing': { 'type': 'string', - 'enum': ['default', 'antialiased', 'none'], + 'enum': ['default', 'antialiased', 'none', 'auto'], 'default': 'default', 'description': - nls.localize('fontAliasing', "Controls font aliasing method in the workbench.\n- default: Sub-pixel font smoothing. On most non-retina displays this will give the sharpest text\n- antialiased: Smooth the font on the level of the pixel, as opposed to the subpixel. Can make the font appear lighter overall\n- none: Disables font smoothing. Text will show with jagged sharp edges"), + nls.localize('fontAliasing', "Controls font aliasing method in the workbench.\n- default: Sub-pixel font smoothing. On most non-retina displays this will give the sharpest text\n- antialiased: Smooth the font on the level of the pixel, as opposed to the subpixel. Can make the font appear lighter overall\n- none: Disables font smoothing. Text will show with jagged sharp edges\n- auto: Applies `default` or `antialiased` automatically based on the DPI of displays."), 'enumDescriptions': [ nls.localize('workbench.fontAliasing.default', "Sub-pixel font smoothing. On most non-retina displays this will give the sharpest text."), nls.localize('workbench.fontAliasing.antialiased', "Smooth the font on the level of the pixel, as opposed to the subpixel. Can make the font appear lighter overall."), - nls.localize('workbench.fontAliasing.none', "Disables font smoothing. Text will show with jagged sharp edges.") + nls.localize('workbench.fontAliasing.none', "Disables font smoothing. Text will show with jagged sharp edges."), + nls.localize('workbench.fontAliasing.auto', "Applies `default` or `antialiased` automatically based on the DPI of displays.") ], 'included': isMacintosh }, diff --git a/src/vs/workbench/electron-browser/media/workbench.css b/src/vs/workbench/electron-browser/media/workbench.css index e3b64be9207..2b45ff41a7b 100644 --- a/src/vs/workbench/electron-browser/media/workbench.css +++ b/src/vs/workbench/electron-browser/media/workbench.css @@ -26,4 +26,18 @@ .monaco-workbench.windows .monaco-action-bar .select-box { margin-top: 7px; /* Center the select box */ +} + +.monaco-font-aliasing-antialiased { + -webkit-font-smoothing: antialiased; +} + +.monaco-font-aliasing-none { + -webkit-font-smoothing: none; +} + +@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) { + .monaco-font-aliasing-auto { + -webkit-font-smoothing: antialiased; + } } \ No newline at end of file diff --git a/src/vs/workbench/electron-browser/workbench.ts b/src/vs/workbench/electron-browser/workbench.ts index e13edb24a93..551bcc802f5 100644 --- a/src/vs/workbench/electron-browser/workbench.ts +++ b/src/vs/workbench/electron-browser/workbench.ts @@ -124,6 +124,8 @@ export interface IWorkbenchStartedInfo { restoredEditors: string[]; } +type FontAliasingOption = 'default' | 'antialiased' | 'none' | 'auto'; + const Identifiers = { WORKBENCH_CONTAINER: 'workbench.main.container', TITLEBAR_PART: 'workbench.parts.titlebar', @@ -202,7 +204,7 @@ export class Workbench implements IPartService { private inZenMode: IContextKey; private sideBarVisibleContext: IContextKey; private hasFilesToCreateOpenOrDiff: boolean; - private fontAliasing: string; + private fontAliasing: FontAliasingOption; private zenMode: { active: boolean; transitionedToFullScreen: boolean; @@ -643,7 +645,7 @@ export class Workbench implements IPartService { this.activityBarHidden = !activityBarVisible; // Font aliasing - this.fontAliasing = this.configurationService.getValue(Workbench.fontAliasingConfigurationKey); + this.fontAliasing = this.configurationService.getValue(Workbench.fontAliasingConfigurationKey); // Zen mode this.zenMode = { @@ -937,10 +939,17 @@ export class Workbench implements IPartService { }); } - private setFontAliasing(aliasing: string) { + private setFontAliasing(aliasing: FontAliasingOption) { this.fontAliasing = aliasing; - - document.body.style['-webkit-font-smoothing'] = (aliasing === 'default' ? '' : aliasing); + const fontAliasingClassNames = [ + 'monaco-font-aliasing-antialiased', + 'monaco-font-aliasing-none', + 'monaco-font-aliasing-auto' + ]; + document.body.classList.remove(...fontAliasingClassNames); + if (aliasing !== 'default') { + document.body.classList.add(`monaco-font-aliasing-${aliasing}`); + } } public dispose(reason = ShutdownReason.QUIT): void { @@ -1088,7 +1097,7 @@ export class Workbench implements IPartService { this.setPanelPositionFromStorageOrConfig(); - const fontAliasing = this.configurationService.getValue(Workbench.fontAliasingConfigurationKey); + const fontAliasing = this.configurationService.getValue(Workbench.fontAliasingConfigurationKey); if (fontAliasing !== this.fontAliasing) { this.setFontAliasing(fontAliasing); } From 1bcfe5b5c70ea4bb8e7b8f648b41b84544ced185 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 22 Jan 2018 19:00:47 +0100 Subject: [PATCH 060/128] fix #41989 --- src/vs/code/electron-main/windows.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/vs/code/electron-main/windows.ts b/src/vs/code/electron-main/windows.ts index 073f7dc441f..8ecf8499910 100644 --- a/src/vs/code/electron-main/windows.ts +++ b/src/vs/code/electron-main/windows.ts @@ -1326,7 +1326,17 @@ export class WindowsManager implements IWindowsMainService { } // Handle untitled workspaces with prompt as needed - e.veto(this.workspacesManager.promptToSaveUntitledWorkspace(this.getWindowById(e.window.id), workspace)); + e.veto(this.workspacesManager.promptToSaveUntitledWorkspace(this.getWindowById(e.window.id), workspace).then(veto => { + if (veto) { + return veto; + } + + // Bug in electron: somehow we need this timeout so that the window closes properly. That + // might be related to the fact that the untitled workspace prompt shows up async and this + // code can execute before the dialog is fully closed which then blocks the window from closing. + // Issue: https://github.com/Microsoft/vscode/issues/41989 + return TPromise.timeout(0).then(() => veto); + })); } public focusLastActive(cli: ParsedArgs, context: OpenContext): CodeWindow { From ad3f4f2784a198c8f4b350a2f79c306a95fc9fc7 Mon Sep 17 00:00:00 2001 From: Peng Lyu Date: Mon, 22 Jan 2018 10:03:19 -0800 Subject: [PATCH 061/128] turn on piece tree for insider. --- src/vs/editor/common/model/textModel.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index 27c58c24d2e..5a05edb5ccc 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -38,7 +38,7 @@ import { PieceTreeTextBufferBuilder } from 'vs/editor/common/model/pieceTreeText import { ChunksTextBufferBuilder } from 'vs/editor/common/model/chunksTextBuffer/chunksTextBufferBuilder'; // Here is the master switch for the text buffer implementation: -const USE_PIECE_TREE_IMPLEMENTATION = false; +const USE_PIECE_TREE_IMPLEMENTATION = true; const USE_CHUNKS_TEXT_BUFFER = false; function createTextBufferBuilder() { From 48a6f7176a89b036f04e3fd44b0824113d40573c Mon Sep 17 00:00:00 2001 From: Ramya Achutha Rao Date: Mon, 22 Jan 2018 10:07:27 -0800 Subject: [PATCH 062/128] Keep notification consistent with recommendations #38543 --- .../electron-browser/extensionTipsService.ts | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensionTipsService.ts b/src/vs/workbench/parts/extensions/electron-browser/extensionTipsService.ts index 619aace599d..e630ffa559a 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/extensionTipsService.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/extensionTipsService.ts @@ -404,25 +404,17 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe return; } - // Suggest the search only once as this is not a strong recommendation - fileExtensionSuggestionIgnoreList.push(fileExtension); - this.storageService.store( - 'extensionsAssistant/fileExtensionsSuggestionIgnore', - JSON.stringify(fileExtensionSuggestionIgnoreList), - StorageScope.GLOBAL - ); - - const message = localize('showLanguageExtensions', "The Marketplace has extensions that can help with '.{0}' files", fileExtension); const searchMarketplaceAction = this.instantiationService.createInstance(ShowLanguageExtensionsAction, fileExtension); const options = [ localize('searchMarketplace', "Search Marketplace"), + choiceNever, choiceClose ]; - this.choiceService.choose(Severity.Info, message, options, 1).done(choice => { + this.choiceService.choose(Severity.Info, message, options, 2).done(choice => { switch (choice) { case 0: /* __GDPR__ @@ -435,6 +427,20 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe searchMarketplaceAction.run(); break; case 1: + fileExtensionSuggestionIgnoreList.push(fileExtension); + this.storageService.store( + 'extensionsAssistant/fileExtensionsSuggestionIgnore', + JSON.stringify(fileExtensionSuggestionIgnoreList), + StorageScope.GLOBAL + ); + /* __GDPR__ + "fileExtensionSuggestion:popup" : { + "userReaction" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "extensionId": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" } + } + */ + this.telemetryService.publicLog('fileExtensionSuggestion:popup', { userReaction: 'neverShowAgain', fileExtension: fileExtension }); + case 2: /* __GDPR__ "fileExtensionSuggestion:popup" : { "userReaction" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, From 81d6f2b1d3f350d3fae0750052b9cc100f166283 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 22 Jan 2018 19:20:29 +0100 Subject: [PATCH 063/128] remove duplicate method --- .../workbench/browser/parts/editor/noTabsTitleControl.ts | 8 +------- src/vs/workbench/browser/parts/editor/tabsTitleControl.ts | 6 ------ src/vs/workbench/browser/parts/editor/titleControl.ts | 2 ++ 3 files changed, 3 insertions(+), 13 deletions(-) diff --git a/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts b/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts index 34cae60469c..8d0c4ef497f 100644 --- a/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts @@ -7,7 +7,7 @@ import 'vs/css!./media/notabstitle'; import errors = require('vs/base/common/errors'); -import { IEditorGroup, toResource } from 'vs/workbench/common/editor'; +import { toResource } from 'vs/workbench/common/editor'; import DOM = require('vs/base/browser/dom'); import { TitleControl } from 'vs/workbench/browser/parts/editor/titleControl'; import { ResourceLabel } from 'vs/workbench/browser/labels'; @@ -19,12 +19,6 @@ export class NoTabsTitleControl extends TitleControl { private titleContainer: HTMLElement; private editorLabel: ResourceLabel; - public setContext(group: IEditorGroup): void { - super.setContext(group); - - this.editorActionsToolbar.context = { group }; - } - public create(parent: HTMLElement): void { super.create(parent); diff --git a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts index 6ac7609d531..892dc38d274 100644 --- a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts @@ -118,12 +118,6 @@ export class TabsTitleControl extends TitleControl { return this.instantiationService.createChild(new ServiceCollection([IWorkbenchEditorService, delegatingEditorService])); } - public setContext(group: IEditorGroup): void { - super.setContext(group); - - this.editorActionsToolbar.context = { group }; - } - public create(parent: HTMLElement): void { super.create(parent); diff --git a/src/vs/workbench/browser/parts/editor/titleControl.ts b/src/vs/workbench/browser/parts/editor/titleControl.ts index 4c2fbffde2a..c7054d7fb5b 100644 --- a/src/vs/workbench/browser/parts/editor/titleControl.ts +++ b/src/vs/workbench/browser/parts/editor/titleControl.ts @@ -170,6 +170,8 @@ export abstract class TitleControl extends Themable implements ITitleAreaControl public setContext(group: IEditorGroup): void { this.context = group; + + this.editorActionsToolbar.context = { group }; } public hasContext(): boolean { From 31bd69224394a264b923c94b3b51877187c19ffa Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 22 Jan 2018 19:46:27 +0100 Subject: [PATCH 064/128] fix #41918 --- src/vs/workbench/browser/actions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/browser/actions.ts b/src/vs/workbench/browser/actions.ts index 12ac79bfc78..e15e84abf60 100644 --- a/src/vs/workbench/browser/actions.ts +++ b/src/vs/workbench/browser/actions.ts @@ -169,7 +169,7 @@ export function prepareActions(actions: IAction[]): IAction[] { for (let l = 0; l < actions.length; l++) { const a = actions[l]; if (types.isUndefinedOrNull(a.order)) { - a.order = lastOrder++; + a.order = ++lastOrder; orderOffset++; } else { a.order += orderOffset; From ec183b09774f7c64a43b0b0f94bbe38efbab44ae Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Mon, 22 Jan 2018 10:46:44 -0800 Subject: [PATCH 065/128] Fix webview protocols for windows drive letter casing --- src/vs/workbench/parts/html/browser/webview.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/parts/html/browser/webview.ts b/src/vs/workbench/parts/html/browser/webview.ts index 90f7cd006cd..4010801932e 100644 --- a/src/vs/workbench/parts/html/browser/webview.ts +++ b/src/vs/workbench/parts/html/browser/webview.ts @@ -420,7 +420,7 @@ function registerFileProtocol( roots: string[] ) { contents.session.protocol.registerFileProtocol(protocol, (request, callback: any) => { - const requestPath = URI.parse(request.url).path; + const requestPath = URI.parse(request.url).fsPath; for (const root of roots) { const normalizedPath = normalize(requestPath, true); if (startsWith(normalizedPath, root + nativeSep)) { From eccf728e6444ceed2ed50e833f6977ca5c2a8c6f Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Mon, 22 Jan 2018 11:45:22 -0800 Subject: [PATCH 066/128] CodeActionScope (#41782) * Add CodeActionScope * Replace matches with contains, try using in ts extension * Move filtering to getCodeActions * Basic test * Docs * Fix tests * Hooking up requested scope * Add basic test for requestedScope * Added auto apply logic * Gate refactor provider to only compute refactorings when requested * Making suggested renames * Clean up code action trigger impl to use single Trrigger info object * Rename codeActionScope file and internal CodeActionScope class * Add quick fix base type * Make keybinding API more similar to insertSnippet Take args as an object instead of as an array of values * Clean up docs * scope -> kind * Fixing examples to match Refactor kind --- .../src/features/refactorProvider.ts | 19 ++++- src/vs/editor/common/modes.ts | 10 ++- .../contrib/quickFix/codeActionTrigger.ts | 32 +++++++ src/vs/editor/contrib/quickFix/quickFix.ts | 9 +- .../contrib/quickFix/quickFixCommands.ts | 85 +++++++++++++++++-- .../editor/contrib/quickFix/quickFixModel.ts | 29 ++++--- .../contrib/quickFix/test/quickFix.test.ts | 51 ++++++++++- .../quickFix/test/quickFixModel.test.ts | 6 +- .../standalone/browser/standaloneLanguages.ts | 9 +- src/vs/monaco.d.ts | 5 ++ src/vs/vscode.d.ts | 74 ++++++++++++++++ .../mainThreadLanguageFeatures.ts | 4 +- src/vs/workbench/api/node/extHost.api.impl.ts | 1 + src/vs/workbench/api/node/extHost.protocol.ts | 3 +- .../workbench/api/node/extHostApiCommands.ts | 3 + .../api/node/extHostLanguageFeatures.ts | 15 ++-- src/vs/workbench/api/node/extHostTypes.ts | 28 ++++++ .../api/extHostLanguageFeatures.test.ts | 30 ++++++- 18 files changed, 372 insertions(+), 41 deletions(-) create mode 100644 src/vs/editor/contrib/quickFix/codeActionTrigger.ts diff --git a/extensions/typescript/src/features/refactorProvider.ts b/extensions/typescript/src/features/refactorProvider.ts index 14dca20a4b5..45e47597017 100644 --- a/extensions/typescript/src/features/refactorProvider.ts +++ b/extensions/typescript/src/features/refactorProvider.ts @@ -108,13 +108,17 @@ export default class TypeScriptRefactorProvider implements vscode.CodeActionProv public async provideCodeActions( document: vscode.TextDocument, _range: vscode.Range, - _context: vscode.CodeActionContext, + context: vscode.CodeActionContext, token: vscode.CancellationToken ): Promise { if (!this.client.apiVersion.has240Features()) { return []; } + if (context.only && !vscode.CodeActionKind.Refactor.contains(context.only)) { + return []; + } + if (!vscode.window.activeTextEditor) { return []; } @@ -146,7 +150,8 @@ export default class TypeScriptRefactorProvider implements vscode.CodeActionProv title: info.description, command: SelectRefactorCommand.ID, arguments: [document, file, info, range] - } + }, + kind: vscode.CodeActionKind.Refactor }); } else { for (const action of info.actions) { @@ -156,7 +161,8 @@ export default class TypeScriptRefactorProvider implements vscode.CodeActionProv title: action.description, command: ApplyRefactoringCommand.ID, arguments: [document, file, info.name, action.name, range] - } + }, + kind: TypeScriptRefactorProvider.getKind(action) }); } } @@ -166,4 +172,11 @@ export default class TypeScriptRefactorProvider implements vscode.CodeActionProv return []; } } + + private static getKind(refactor: Proto.RefactorActionInfo) { + if (refactor.name.startsWith('function_')) { + return vscode.CodeActionKind.RefactorExtract.append('function'); + } + return vscode.CodeActionKind.Refactor; + } } \ No newline at end of file diff --git a/src/vs/editor/common/modes.ts b/src/vs/editor/common/modes.ts index c3c19319c48..6b573d085a9 100644 --- a/src/vs/editor/common/modes.ts +++ b/src/vs/editor/common/modes.ts @@ -343,6 +343,14 @@ export interface CodeAction { command?: Command; edit?: WorkspaceEdit; diagnostics?: IMarkerData[]; + kind?: string; +} + +/** + * @internal + */ +export interface CodeActionContext { + only?: string; } /** @@ -354,7 +362,7 @@ export interface CodeActionProvider { /** * Provide commands for the given document and range. */ - provideCodeActions(model: model.ITextModel, range: Range, token: CancellationToken): CodeAction[] | Thenable; + provideCodeActions(model: model.ITextModel, range: Range, context: CodeActionContext, token: CancellationToken): CodeAction[] | Thenable; } /** diff --git a/src/vs/editor/contrib/quickFix/codeActionTrigger.ts b/src/vs/editor/contrib/quickFix/codeActionTrigger.ts new file mode 100644 index 00000000000..0cd81d55df8 --- /dev/null +++ b/src/vs/editor/contrib/quickFix/codeActionTrigger.ts @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { startsWith } from 'vs/base/common/strings'; + +export class CodeActionKind { + private static readonly sep = '.'; + + public static readonly Empty = new CodeActionKind(''); + + constructor( + public readonly value: string + ) { } + + public contains(other: string): boolean { + return this.value === other || startsWith(other, this.value + CodeActionKind.sep); + } +} + +export enum CodeActionAutoApply { + IfSingle = 1, + First = 2, + Never = 3 +} + +export interface CodeActionTrigger { + type: 'auto' | 'manual'; + kind?: CodeActionKind; + autoApply?: CodeActionAutoApply; +} \ No newline at end of file diff --git a/src/vs/editor/contrib/quickFix/quickFix.ts b/src/vs/editor/contrib/quickFix/quickFix.ts index 8e619e15c03..10bbbe4d5e8 100644 --- a/src/vs/editor/contrib/quickFix/quickFix.ts +++ b/src/vs/editor/contrib/quickFix/quickFix.ts @@ -14,16 +14,19 @@ import { onUnexpectedExternalError, illegalArgument } from 'vs/base/common/error import { IModelService } from 'vs/editor/common/services/modelService'; import { registerLanguageCommand } from 'vs/editor/browser/editorExtensions'; import { isFalsyOrEmpty } from 'vs/base/common/arrays'; +import { CodeActionKind } from './codeActionTrigger'; -export function getCodeActions(model: ITextModel, range: Range): TPromise { +export function getCodeActions(model: ITextModel, range: Range, scope?: CodeActionKind): TPromise { const allResults: CodeAction[] = []; const promises = CodeActionProviderRegistry.all(model).map(support => { - return asWinJsPromise(token => support.provideCodeActions(model, range, token)).then(result => { + return asWinJsPromise(token => support.provideCodeActions(model, range, { only: scope ? scope.value : undefined }, token)).then(result => { if (Array.isArray(result)) { for (const quickFix of result) { if (quickFix) { - allResults.push(quickFix); + if (!scope || (quickFix.kind && scope.contains(quickFix.kind))) { + allResults.push(quickFix); + } } } } diff --git a/src/vs/editor/contrib/quickFix/quickFixCommands.ts b/src/vs/editor/contrib/quickFix/quickFixCommands.ts index 2d067db0bf0..9850c794400 100644 --- a/src/vs/editor/contrib/quickFix/quickFixCommands.ts +++ b/src/vs/editor/contrib/quickFix/quickFixCommands.ts @@ -15,11 +15,12 @@ import { optional } from 'vs/platform/instantiation/common/instantiation'; import { IMarkerService } from 'vs/platform/markers/common/markers'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; -import { registerEditorAction, registerEditorContribution, ServicesAccessor, EditorAction } from 'vs/editor/browser/editorExtensions'; +import { registerEditorAction, registerEditorContribution, ServicesAccessor, EditorAction, EditorCommand, registerEditorCommand } from 'vs/editor/browser/editorExtensions'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { QuickFixContextMenu } from './quickFixWidget'; import { LightBulbWidget } from './lightBulbWidget'; import { QuickFixModel, QuickFixComputeEvent } from './quickFixModel'; +import { CodeActionKind, CodeActionAutoApply } from './codeActionTrigger'; import { TPromise } from 'vs/base/common/winjs.base'; import { CodeAction } from 'vs/editor/common/modes'; import { createBulkEdit } from 'vs/editor/browser/services/bulkEdit'; @@ -57,7 +58,7 @@ export class QuickFixController implements IEditorContribution { this._updateLightBulbTitle(); this._disposables.push( - this._quickFixContextMenu.onDidExecuteCodeAction(_ => this._model.trigger('auto')), + this._quickFixContextMenu.onDidExecuteCodeAction(_ => this._model.trigger({ type: 'auto' })), this._lightBulbWidget.onClick(this._handleLightBulbSelect, this), this._model.onDidChangeFixes(e => this._onQuickFixEvent(e)), this._keybindingService.onDidUpdateKeybindings(this._updateLightBulbTitle, this) @@ -70,9 +71,21 @@ export class QuickFixController implements IEditorContribution { } private _onQuickFixEvent(e: QuickFixComputeEvent): void { - if (e && e.type === 'manual') { - this._quickFixContextMenu.show(e.fixes, e.position); + if (e && e.trigger.kind) { + // Triggered for specific scope + // Apply if we only have one action or requested autoApply, otherwise show menu + e.fixes.then(fixes => { + if (e.trigger.autoApply === CodeActionAutoApply.First || (e.trigger.autoApply === CodeActionAutoApply.IfSingle && fixes.length === 1)) { + this._onApplyCodeAction(fixes[0]); + } else { + this._quickFixContextMenu.show(e.fixes, e.position); + } + }); + return; + } + if (e && e.trigger.type === 'manual') { + this._quickFixContextMenu.show(e.fixes, e.position); } else if (e && e.fixes) { // auto magically triggered // * update an existing list of code actions @@ -96,7 +109,11 @@ export class QuickFixController implements IEditorContribution { } public triggerFromEditorSelection(): void { - this._model.trigger('manual'); + this._model.trigger({ type: 'manual' }); + } + + public triggerCodeActionFromEditorSelection(kind?: CodeActionKind, autoApply?: CodeActionAutoApply): void { + this._model.trigger({ type: 'manual', kind, autoApply }); } private _updateLightBulbTitle(): void { @@ -148,5 +165,63 @@ export class QuickFixAction extends EditorAction { } } + +class CodeActionCommandArgs { + public static fromUser(arg: any): CodeActionCommandArgs { + if (!arg || typeof arg !== 'object') { + return new CodeActionCommandArgs(CodeActionKind.Empty, CodeActionAutoApply.IfSingle); + } + return new CodeActionCommandArgs( + CodeActionCommandArgs.getKindFromUser(arg), + CodeActionCommandArgs.getApplyFromUser(arg)); + } + + private static getApplyFromUser(arg: any) { + switch (typeof arg.apply === 'string' ? arg.apply.toLowerCase() : '') { + case 'first': + return CodeActionAutoApply.First; + + case 'never': + return CodeActionAutoApply.Never; + + case 'ifsingle': + default: + return CodeActionAutoApply.IfSingle; + } + } + + private static getKindFromUser(arg: any) { + return typeof arg.kind === 'string' + ? new CodeActionKind(arg.kind) + : CodeActionKind.Empty; + } + + private constructor( + public readonly kind: CodeActionKind, + public readonly apply: CodeActionAutoApply + ) { } +} + +export class CodeActionCommand extends EditorCommand { + + static readonly Id = 'editor.action.codeAction'; + + constructor() { + super({ + id: CodeActionCommand.Id, + precondition: ContextKeyExpr.and(EditorContextKeys.writable, EditorContextKeys.hasCodeActionsProvider) + }); + } + + public runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, userArg: any) { + const controller = QuickFixController.get(editor); + if (controller) { + const args = CodeActionCommandArgs.fromUser(userArg); + controller.triggerCodeActionFromEditorSelection(args.kind, args.apply); + } + } +} + registerEditorContribution(QuickFixController); registerEditorAction(QuickFixAction); +registerEditorCommand(new CodeActionCommand()); diff --git a/src/vs/editor/contrib/quickFix/quickFixModel.ts b/src/vs/editor/contrib/quickFix/quickFixModel.ts index e2ff0dcd2a1..3e4366c4d63 100644 --- a/src/vs/editor/contrib/quickFix/quickFixModel.ts +++ b/src/vs/editor/contrib/quickFix/quickFixModel.ts @@ -13,6 +13,7 @@ import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; import { CodeActionProviderRegistry, CodeAction } from 'vs/editor/common/modes'; import { getCodeActions } from './quickFix'; +import { CodeActionTrigger } from './codeActionTrigger'; import { Position } from 'vs/editor/common/core/position'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; @@ -36,26 +37,26 @@ export class QuickFixOracle { this._disposables = dispose(this._disposables); } - trigger(type: 'manual' | 'auto'): void { + trigger(trigger: CodeActionTrigger): void { let rangeOrSelection = this._getRangeOfMarker() || this._getRangeOfSelectionUnlessWhitespaceEnclosed(); - if (!rangeOrSelection && type === 'manual') { + if (!rangeOrSelection && trigger.type === 'manual') { rangeOrSelection = this._editor.getSelection(); } - this._createEventAndSignalChange(type, rangeOrSelection); + this._createEventAndSignalChange(trigger, rangeOrSelection); } private _onMarkerChanges(resources: URI[]): void { const { uri } = this._editor.getModel(); for (const resource of resources) { if (resource.toString() === uri.toString()) { - this.trigger('auto'); + this.trigger({ type: 'auto' }); return; } } } private _onCursorChange(): void { - this.trigger('auto'); + this.trigger({ type: 'auto' }); } private _getRangeOfMarker(): Range { @@ -98,24 +99,24 @@ export class QuickFixOracle { return selection; } - private _createEventAndSignalChange(type: 'auto' | 'manual', rangeOrSelection: Range | Selection): void { + private _createEventAndSignalChange(trigger: CodeActionTrigger, rangeOrSelection: Range | Selection): void { if (!rangeOrSelection) { // cancel this._signalChange({ - type, + trigger, range: undefined, position: undefined, - fixes: undefined + fixes: undefined, }); } else { // actual const model = this._editor.getModel(); const range = model.validateRange(rangeOrSelection); const position = rangeOrSelection instanceof Selection ? rangeOrSelection.getPosition() : rangeOrSelection.getStartPosition(); - const fixes = getCodeActions(model, range); + const fixes = getCodeActions(model, range, trigger && trigger.kind); this._signalChange({ - type, + trigger, range, position, fixes @@ -125,7 +126,7 @@ export class QuickFixOracle { } export interface QuickFixComputeEvent { - type: 'auto' | 'manual'; + trigger: CodeActionTrigger; range: Range; position: Position; fixes: TPromise; @@ -172,13 +173,13 @@ export class QuickFixModel { && !this._editor.getConfiguration().readOnly) { this._quickFixOracle = new QuickFixOracle(this._editor, this._markerService, p => this._onDidChangeFixes.fire(p)); - this._quickFixOracle.trigger('auto'); + this._quickFixOracle.trigger({ type: 'auto' }); } } - trigger(type: 'auto' | 'manual'): void { + trigger(trigger: CodeActionTrigger): void { if (this._quickFixOracle) { - this._quickFixOracle.trigger(type); + this._quickFixOracle.trigger(trigger); } } } diff --git a/src/vs/editor/contrib/quickFix/test/quickFix.test.ts b/src/vs/editor/contrib/quickFix/test/quickFix.test.ts index 8e1fdc79473..ba52da9dde7 100644 --- a/src/vs/editor/contrib/quickFix/test/quickFix.test.ts +++ b/src/vs/editor/contrib/quickFix/test/quickFix.test.ts @@ -8,10 +8,11 @@ import * as assert from 'assert'; import URI from 'vs/base/common/uri'; import Severity from 'vs/base/common/severity'; import { TextModel } from 'vs/editor/common/model/textModel'; -import { CodeActionProviderRegistry, LanguageIdentifier, CodeActionProvider, Command, WorkspaceEdit, IResourceEdit } from 'vs/editor/common/modes'; +import { CodeActionProviderRegistry, LanguageIdentifier, CodeActionProvider, Command, WorkspaceEdit, IResourceEdit, CodeAction, CodeActionContext } from 'vs/editor/common/modes'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { Range } from 'vs/editor/common/core/range'; import { getCodeActions } from 'vs/editor/contrib/quickFix/quickFix'; +import { CodeActionKind } from 'vs/editor/contrib/quickFix/codeActionTrigger'; suite('QuickFix', () => { @@ -120,4 +121,52 @@ suite('QuickFix', () => { assert.equal(actions.length, 6); assert.deepEqual(actions, expected); }); + + test('getCodeActions should filter by scope', async function () { + const provider = new class implements CodeActionProvider { + provideCodeActions(): CodeAction[] { + return [ + { title: 'a', kind: 'a' }, + { title: 'b', kind: 'b' }, + { title: 'a.b', kind: 'a.b' } + ]; + } + }; + + disposables.push(CodeActionProviderRegistry.register('fooLang', provider)); + + { + const actions = await getCodeActions(model, new Range(1, 1, 2, 1), new CodeActionKind('a')); + assert.equal(actions.length, 2); + assert.strictEqual(actions[0].title, 'a'); + assert.strictEqual(actions[1].title, 'a.b'); + } + + { + const actions = await getCodeActions(model, new Range(1, 1, 2, 1), new CodeActionKind('a.b')); + assert.equal(actions.length, 1); + assert.strictEqual(actions[0].title, 'a.b'); + } + + { + const actions = await getCodeActions(model, new Range(1, 1, 2, 1), new CodeActionKind('a.b.c')); + assert.equal(actions.length, 0); + } + }); + + test('getCodeActions should forward requested scope to providers', async function () { + const provider = new class implements CodeActionProvider { + provideCodeActions(_model: any, _range: Range, context: CodeActionContext, _token: any): CodeAction[] { + return [ + { title: context.only, kind: context.only } + ]; + } + }; + + disposables.push(CodeActionProviderRegistry.register('fooLang', provider)); + + const actions = await getCodeActions(model, new Range(1, 1, 2, 1), new CodeActionKind('a')); + assert.equal(actions.length, 1); + assert.strictEqual(actions[0].title, 'a'); + }); }); diff --git a/src/vs/editor/contrib/quickFix/test/quickFixModel.test.ts b/src/vs/editor/contrib/quickFix/test/quickFixModel.test.ts index c2806d7d31e..a6c3b4663a7 100644 --- a/src/vs/editor/contrib/quickFix/test/quickFixModel.test.ts +++ b/src/vs/editor/contrib/quickFix/test/quickFixModel.test.ts @@ -47,7 +47,7 @@ suite('QuickFix', () => { test('Orcale -> marker added', done => { const oracle = new QuickFixOracle(editor, markerService, e => { - assert.equal(e.type, 'auto'); + assert.equal(e.trigger.type, 'auto'); assert.ok(e.fixes); e.fixes.then(fixes => { @@ -83,7 +83,7 @@ suite('QuickFix', () => { return new Promise((resolve, reject) => { const oracle = new QuickFixOracle(editor, markerService, e => { - assert.equal(e.type, 'auto'); + assert.equal(e.trigger.type, 'auto'); assert.ok(e.fixes); e.fixes.then(fixes => { oracle.dispose(); @@ -160,7 +160,7 @@ suite('QuickFix', () => { await new Promise(resolve => { let oracle = new QuickFixOracle(editor, markerService, e => { - assert.equal(e.type, 'auto'); + assert.equal(e.trigger.type, 'auto'); assert.deepEqual(e.range, { startLineNumber: 3, startColumn: 1, endLineNumber: 3, endColumn: 4 }); assert.deepEqual(e.position, { lineNumber: 3, column: 1 }); diff --git a/src/vs/editor/standalone/browser/standaloneLanguages.ts b/src/vs/editor/standalone/browser/standaloneLanguages.ts index 5c5b72caa39..1e4f3f4fc29 100644 --- a/src/vs/editor/standalone/browser/standaloneLanguages.ts +++ b/src/vs/editor/standalone/browser/standaloneLanguages.ts @@ -329,11 +329,11 @@ export function registerCodeLensProvider(languageId: string, provider: modes.Cod */ export function registerCodeActionProvider(languageId: string, provider: CodeActionProvider): IDisposable { return modes.CodeActionProviderRegistry.register(languageId, { - provideCodeActions: (model: model.ITextModel, range: Range, token: CancellationToken): (modes.Command | modes.CodeAction)[] | Thenable<(modes.Command | modes.CodeAction)[]> => { + provideCodeActions: (model: model.ITextModel, range: Range, context: modes.CodeActionContext, token: CancellationToken): (modes.Command | modes.CodeAction)[] | Thenable<(modes.Command | modes.CodeAction)[]> => { let markers = StaticServices.markerService.get().read({ resource: model.uri }).filter(m => { return Range.areIntersectingOrTouching(m, range); }); - return provider.provideCodeActions(model, range, { markers }, token); + return provider.provideCodeActions(model, range, { markers, only: context.only }, token); } }); } @@ -401,6 +401,11 @@ export interface CodeActionContext { * @readonly */ readonly markers: IMarkerData[]; + + /** + * Requested kind of actions to return. + */ + readonly only?: string; } /** diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index b419e7e58ba..dd87b203a17 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -4043,6 +4043,10 @@ declare module monaco.languages { * @readonly */ readonly markers: editor.IMarkerData[]; + /** + * Requested kind of actions to return. + */ + readonly only?: string; } /** @@ -4495,6 +4499,7 @@ declare module monaco.languages { command?: Command; edit?: WorkspaceEdit; diagnostics?: editor.IMarkerData[]; + kind?: string; } /** diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index 4f851542eff..b0582117933 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -1812,6 +1812,66 @@ declare module 'vscode' { */ export type ProviderResult = T | undefined | null | Thenable; + /** + * Kind of a code action. + * + * Kinds are a hierarchical list of identifiers separated by `.`, e.g. `"refactor.extract.function"`. + */ + export class CodeActionKind { + /** + * Empty kind. + */ + static readonly Empty: CodeActionKind; + + /** + * Base kind for quickfix actions. + */ + static readonly QuickFix: CodeActionKind; + + /** + * Base kind for refactoring actions. + */ + static readonly Refactor: CodeActionKind; + + /** + * Base kind for refactoring extraction actions. + */ + static readonly RefactorExtract: CodeActionKind; + + /** + * Base kind for refactoring inline actions. + */ + static readonly RefactorInline: CodeActionKind; + + /** + * Base kind for refactoring rewite actions. + */ + static readonly RefactorRewrite: CodeActionKind; + + private constructor(value: string); + + /** + * String value of the kind, e.g. `"refactor.extract.function"`. + */ + readonly value?: string; + + /** + * Create a new kind by appending a more specific selector to the current kind. + * + * Does not modify the current kind. + */ + append(parts: string): CodeActionKind; + + /** + * Does this kind contain `other`? + * + * The kind `"refactor"` for example contains `"refactor.extract"` and ``"refactor.extract.function"`, but not `"unicorn.refactor.extract"` or `"refactory.extract"` + * + * @param other Kind to check. + */ + contains(other: CodeActionKind): boolean; + } + /** * Contains additional diagnostic information about the context in which * a [code action](#CodeActionProvider.provideCodeActions) is run. @@ -1821,6 +1881,13 @@ declare module 'vscode' { * An array of diagnostics. */ readonly diagnostics: Diagnostic[]; + + /** + * Requested kind of actions to return. + * + * Actions not of this kind are filtered out before being shown by the lightbulb. + */ + readonly only?: CodeActionKind; } /** @@ -1853,6 +1920,13 @@ declare module 'vscode' { */ command?: Command; + /** + * Kind of the code action. + * + * Used to filter code actions. + */ + readonly kind?: CodeActionKind; + /** * Creates a new code action. * diff --git a/src/vs/workbench/api/electron-browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/electron-browser/mainThreadLanguageFeatures.ts index d60034d69a2..1b6408a0ec8 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadLanguageFeatures.ts @@ -206,8 +206,8 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha $registerQuickFixSupport(handle: number, selector: vscode.DocumentSelector): void { this._registrations[handle] = modes.CodeActionProviderRegistry.register(toLanguageSelector(selector), { - provideCodeActions: (model: ITextModel, range: EditorRange, token: CancellationToken): Thenable => { - return this._heapService.trackRecursive(wireCancellationToken(token, this._proxy.$provideCodeActions(handle, model.uri, range))).then(MainThreadLanguageFeatures._reviveCodeActionDto); + provideCodeActions: (model: ITextModel, range: EditorRange, context: modes.CodeActionContext, token: CancellationToken): Thenable => { + return this._heapService.trackRecursive(wireCancellationToken(token, this._proxy.$provideCodeActions(handle, model.uri, range, context))).then(MainThreadLanguageFeatures._reviveCodeActionDto); } }); } diff --git a/src/vs/workbench/api/node/extHost.api.impl.ts b/src/vs/workbench/api/node/extHost.api.impl.ts index 4d6eb312357..cc7aafe634b 100644 --- a/src/vs/workbench/api/node/extHost.api.impl.ts +++ b/src/vs/workbench/api/node/extHost.api.impl.ts @@ -556,6 +556,7 @@ export function createApiFactory( Breakpoint: extHostTypes.Breakpoint, CancellationTokenSource: CancellationTokenSource, CodeAction: extHostTypes.CodeAction, + CodeActionKind: extHostTypes.CodeActionKind, CodeLens: extHostTypes.CodeLens, Color: extHostTypes.Color, ColorPresentation: extHostTypes.ColorPresentation, diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index ea9d6e8b960..d4dcd03d363 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -639,6 +639,7 @@ export interface CodeActionDto { edit?: WorkspaceEditDto; diagnostics?: IMarkerData[]; command?: modes.Command; + scope?: string; } export interface ExtHostLanguageFeaturesShape { @@ -651,7 +652,7 @@ export interface ExtHostLanguageFeaturesShape { $provideHover(handle: number, resource: UriComponents, position: IPosition): TPromise; $provideDocumentHighlights(handle: number, resource: UriComponents, position: IPosition): TPromise; $provideReferences(handle: number, resource: UriComponents, position: IPosition, context: modes.ReferenceContext): TPromise; - $provideCodeActions(handle: number, resource: UriComponents, range: IRange): TPromise; + $provideCodeActions(handle: number, resource: UriComponents, range: IRange, context: modes.CodeActionContext): TPromise; $provideDocumentFormattingEdits(handle: number, resource: UriComponents, options: modes.FormattingOptions): TPromise; $provideDocumentRangeFormattingEdits(handle: number, resource: UriComponents, range: IRange, options: modes.FormattingOptions): TPromise; $provideOnTypeFormattingEdits(handle: number, resource: UriComponents, position: IPosition, ch: string, options: modes.FormattingOptions): TPromise; diff --git a/src/vs/workbench/api/node/extHostApiCommands.ts b/src/vs/workbench/api/node/extHostApiCommands.ts index bf16fdb2c52..9790068a020 100644 --- a/src/vs/workbench/api/node/extHostApiCommands.ts +++ b/src/vs/workbench/api/node/extHostApiCommands.ts @@ -419,6 +419,9 @@ export class ExtHostApiCommands { codeAction.title, typeConverters.WorkspaceEdit.to(codeAction.edit) ); + if (codeAction.kind) { + ret.scope = new types.CodeActionKind(codeAction.kind); + } return ret; } }); diff --git a/src/vs/workbench/api/node/extHostLanguageFeatures.ts b/src/vs/workbench/api/node/extHostLanguageFeatures.ts index 37ac8015683..9ebfc217656 100644 --- a/src/vs/workbench/api/node/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/node/extHostLanguageFeatures.ts @@ -9,7 +9,7 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { mixin } from 'vs/base/common/objects'; import * as vscode from 'vscode'; import * as TypeConverters from 'vs/workbench/api/node/extHostTypeConverters'; -import { Range, Disposable, CompletionList, SnippetString, Color } from 'vs/workbench/api/node/extHostTypes'; +import { Range, Disposable, CompletionList, SnippetString, Color, CodeActionKind } from 'vs/workbench/api/node/extHostTypes'; import { ISingleEditOperation } from 'vs/editor/common/model'; import * as modes from 'vs/editor/common/modes'; import { ExtHostHeapService } from 'vs/workbench/api/node/extHostHeapService'; @@ -273,7 +273,7 @@ class CodeActionAdapter { this._provider = provider; } - provideCodeActions(resource: URI, range: IRange): TPromise { + provideCodeActions(resource: URI, range: IRange, context: modes.CodeActionContext): TPromise { const doc = this._documents.getDocumentData(resource).document; const ran = TypeConverters.toRange(range); @@ -289,8 +289,12 @@ class CodeActionAdapter { } }); + const codeActionContext: vscode.CodeActionContext = { + diagnostics: allDiagnostics, + only: context.only ? new CodeActionKind(context.only) : undefined + }; return asWinJsPromise(token => - this._provider.provideCodeActions(doc, ran, { diagnostics: allDiagnostics }, token) + this._provider.provideCodeActions(doc, ran, codeActionContext, token) ).then(commandsOrActions => { if (isFalsyOrEmpty(commandsOrActions)) { return undefined; @@ -314,6 +318,7 @@ class CodeActionAdapter { command: candidate.command && this._commands.toInternal(candidate.command), diagnostics: candidate.diagnostics && candidate.diagnostics.map(DiagnosticCollection.toMarkerData), edit: candidate.edit && TypeConverters.WorkspaceEdit.from(candidate.edit), + kind: candidate.kind && candidate.kind.value }); } } @@ -943,8 +948,8 @@ export class ExtHostLanguageFeatures implements ExtHostLanguageFeaturesShape { return this._createDisposable(handle); } - $provideCodeActions(handle: number, resource: UriComponents, range: IRange): TPromise { - return this._withAdapter(handle, CodeActionAdapter, adapter => adapter.provideCodeActions(URI.revive(resource), range)); + $provideCodeActions(handle: number, resource: UriComponents, range: IRange, context: modes.CodeActionContext): TPromise { + return this._withAdapter(handle, CodeActionAdapter, adapter => adapter.provideCodeActions(URI.revive(resource), range, context)); } // --- formatting diff --git a/src/vs/workbench/api/node/extHostTypes.ts b/src/vs/workbench/api/node/extHostTypes.ts index 76c30af3a0f..f09585c5473 100644 --- a/src/vs/workbench/api/node/extHostTypes.ts +++ b/src/vs/workbench/api/node/extHostTypes.ts @@ -12,6 +12,7 @@ import * as vscode from 'vscode'; import { isMarkdownString } from 'vs/base/common/htmlContent'; import { IRelativePattern } from 'vs/base/common/glob'; import { relative } from 'path'; +import { startsWith } from 'vs/base/common/strings'; export class Disposable { @@ -818,12 +819,39 @@ export class CodeAction { dianostics?: Diagnostic[]; + scope?: CodeActionKind; + constructor(title: string, edit?: WorkspaceEdit) { this.title = title; this.edit = edit; } } + +export class CodeActionKind { + private static readonly sep = '.'; + + public static readonly Empty = new CodeActionKind(''); + public static readonly QuickFix = new CodeActionKind('quickfix'); + public static readonly Refactor = new CodeActionKind('refactor'); + public static readonly RefactorExtract = CodeActionKind.Refactor.append('extract'); + public static readonly RefactorInline = CodeActionKind.Refactor.append('inline'); + public static readonly RefactorRewrite = CodeActionKind.Refactor.append('rewrite'); + + constructor( + public readonly value: string + ) { } + + public append(parts: string): CodeActionKind { + return new CodeActionKind(this.value ? this.value + CodeActionKind.sep + parts : parts); + } + + public contains(other: CodeActionKind): boolean { + return this.value === other.value || startsWith(other.value, this.value + CodeActionKind.sep); + } +} + + export class CodeLens { range: Range; diff --git a/src/vs/workbench/test/electron-browser/api/extHostLanguageFeatures.test.ts b/src/vs/workbench/test/electron-browser/api/extHostLanguageFeatures.test.ts index 256ebb6879f..e86703048ec 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostLanguageFeatures.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostLanguageFeatures.test.ts @@ -641,7 +641,7 @@ suite('ExtHostLanguageFeatures', function () { // --- quick fix - test('Quick Fix, data conversion', function () { + test('Quick Fix, command data conversion', function () { disposables.push(extHost.registerCodeActionProvider(defaultSelector, { provideCodeActions(): vscode.Command[] { @@ -665,6 +665,34 @@ suite('ExtHostLanguageFeatures', function () { }); }); + test('Quick Fix, code action data conversion', function () { + + disposables.push(extHost.registerCodeActionProvider(defaultSelector, { + provideCodeActions(): vscode.CodeAction[] { + return [ + { + title: 'Testing1', + command: { title: 'Testing1Command', command: 'test1' }, + kind: types.CodeActionKind.Empty.append('test.scope') + } + ]; + } + })); + + return rpcProtocol.sync().then(() => { + return getCodeActions(model, model.getFullModelRange()).then(value => { + assert.equal(value.length, 1); + + const [first] = value; + assert.equal(first.title, 'Testing1'); + assert.equal(first.command.title, 'Testing1Command'); + assert.equal(first.command.id, 'test1'); + assert.equal(first.kind, 'test.scope'); + }); + }); + }); + + test('Cannot read property \'id\' of undefined, #29469', function () { disposables.push(extHost.registerCodeActionProvider(defaultSelector, { From 3cb83387be101414346eb40c820eeb26d09e0b70 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Mon, 22 Jan 2018 11:48:35 -0800 Subject: [PATCH 067/128] Use CodeActionKind.append --- src/vs/workbench/api/node/extHostTypes.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/api/node/extHostTypes.ts b/src/vs/workbench/api/node/extHostTypes.ts index f09585c5473..954042a7958 100644 --- a/src/vs/workbench/api/node/extHostTypes.ts +++ b/src/vs/workbench/api/node/extHostTypes.ts @@ -832,8 +832,8 @@ export class CodeActionKind { private static readonly sep = '.'; public static readonly Empty = new CodeActionKind(''); - public static readonly QuickFix = new CodeActionKind('quickfix'); - public static readonly Refactor = new CodeActionKind('refactor'); + public static readonly QuickFix = CodeActionKind.Empty.append('quickfix'); + public static readonly Refactor = CodeActionKind.Empty.append('refactor'); public static readonly RefactorExtract = CodeActionKind.Refactor.append('extract'); public static readonly RefactorInline = CodeActionKind.Refactor.append('inline'); public static readonly RefactorRewrite = CodeActionKind.Refactor.append('rewrite'); From 6316a4d0c810e24818b0810c8290b82ac5ddacfe Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Mon, 22 Jan 2018 11:59:17 -0800 Subject: [PATCH 068/128] Take kind instead of edit in CodeAction ctor Changes the `CodeAction` constructor to take a kind instead of an edit. This makes the API more consistent IMO, as now both `edit` and `command` are set the same way --- .../src/features/quickFixProvider.ts | 9 +++--- .../src/features/refactorProvider.ts | 28 ++++++++----------- src/vs/vscode.d.ts | 8 +++--- .../workbench/api/node/extHostApiCommands.ts | 6 ++-- src/vs/workbench/api/node/extHostTypes.ts | 6 ++-- 5 files changed, 26 insertions(+), 31 deletions(-) diff --git a/extensions/typescript/src/features/quickFixProvider.ts b/extensions/typescript/src/features/quickFixProvider.ts index 4c4a86d1d97..d83c23d64da 100644 --- a/extensions/typescript/src/features/quickFixProvider.ts +++ b/extensions/typescript/src/features/quickFixProvider.ts @@ -131,10 +131,8 @@ export default class TypeScriptQuickFixProvider implements vscode.CodeActionProv diagnostic: vscode.Diagnostic, tsAction: Proto.CodeFixAction ): vscode.CodeAction { - const codeAction = new vscode.CodeAction( - tsAction.description, - getEditForCodeAction(this.client, tsAction)); - + const codeAction = new vscode.CodeAction(tsAction.description, vscode.CodeActionKind.QuickFix); + codeAction.edit = getEditForCodeAction(this.client, tsAction); codeAction.diagnostics = [diagnostic]; if (tsAction.commands) { codeAction.command = { @@ -172,7 +170,8 @@ export default class TypeScriptQuickFixProvider implements vscode.CodeActionProv const codeAction = new vscode.CodeAction( localize('fixAllInFileLabel', '{0} (Fix all in file)', tsAction.description), - createWorkspaceEditFromFileCodeEdits(this.client, combinedCodeFixesResponse.body.changes)); + vscode.CodeActionKind.QuickFix); + codeAction.edit = createWorkspaceEditFromFileCodeEdits(this.client, combinedCodeFixesResponse.body.changes); codeAction.diagnostics = [diagnostic]; if (tsAction.commands) { codeAction.command = { diff --git a/extensions/typescript/src/features/refactorProvider.ts b/extensions/typescript/src/features/refactorProvider.ts index 45e47597017..088d8879059 100644 --- a/extensions/typescript/src/features/refactorProvider.ts +++ b/extensions/typescript/src/features/refactorProvider.ts @@ -144,26 +144,22 @@ export default class TypeScriptRefactorProvider implements vscode.CodeActionProv const actions: vscode.CodeAction[] = []; for (const info of response.body) { if (info.inlineable === false) { - actions.push({ + const codeAction = new vscode.CodeAction(info.description, vscode.CodeActionKind.Refactor); + codeAction.command = { title: info.description, - command: { - title: info.description, - command: SelectRefactorCommand.ID, - arguments: [document, file, info, range] - }, - kind: vscode.CodeActionKind.Refactor - }); + command: SelectRefactorCommand.ID, + arguments: [document, file, info, range] + }; + actions.push(codeAction); } else { for (const action of info.actions) { - actions.push({ + const codeAction = new vscode.CodeAction(action.description, TypeScriptRefactorProvider.getKind(action)); + codeAction.command = { title: action.description, - command: { - title: action.description, - command: ApplyRefactoringCommand.ID, - arguments: [document, file, info.name, action.name, range] - }, - kind: TypeScriptRefactorProvider.getKind(action) - }); + command: ApplyRefactoringCommand.ID, + arguments: [document, file, info.name, action.name, range] + }; + actions.push(codeAction); } } } diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index b0582117933..184c8b3f110 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -1897,7 +1897,7 @@ declare module 'vscode' { export class CodeAction { /** - * A short, human-readanle, title for this code action. + * A short, human-readable, title for this code action. */ title: string; @@ -1925,7 +1925,7 @@ declare module 'vscode' { * * Used to filter code actions. */ - readonly kind?: CodeActionKind; + kind?: CodeActionKind; /** * Creates a new code action. @@ -1934,9 +1934,9 @@ declare module 'vscode' { * or a [command](#CodeAction.command). * * @param title The title of the code action. - * @param edits The edit of the code action. + * @param kind The kind of the code action. */ - constructor(title: string, edit?: WorkspaceEdit); + constructor(title: string, kind?: CodeActionKind); } /** diff --git a/src/vs/workbench/api/node/extHostApiCommands.ts b/src/vs/workbench/api/node/extHostApiCommands.ts index 9790068a020..f95fca70c42 100644 --- a/src/vs/workbench/api/node/extHostApiCommands.ts +++ b/src/vs/workbench/api/node/extHostApiCommands.ts @@ -417,10 +417,10 @@ export class ExtHostApiCommands { } else { const ret = new types.CodeAction( codeAction.title, - typeConverters.WorkspaceEdit.to(codeAction.edit) + codeAction.kind ? new types.CodeActionKind(codeAction.kind) : undefined ); - if (codeAction.kind) { - ret.scope = new types.CodeActionKind(codeAction.kind); + if (codeAction.edit) { + ret.edit = typeConverters.WorkspaceEdit.to(codeAction.edit); } return ret; } diff --git a/src/vs/workbench/api/node/extHostTypes.ts b/src/vs/workbench/api/node/extHostTypes.ts index 954042a7958..761ffee7608 100644 --- a/src/vs/workbench/api/node/extHostTypes.ts +++ b/src/vs/workbench/api/node/extHostTypes.ts @@ -819,11 +819,11 @@ export class CodeAction { dianostics?: Diagnostic[]; - scope?: CodeActionKind; + kind?: CodeActionKind; - constructor(title: string, edit?: WorkspaceEdit) { + constructor(title: string, kind?: CodeActionKind) { this.title = title; - this.edit = edit; + this.kind = kind; } } From 80991ce8a05bc57d2fd1b1c0451860fb1c47579d Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 22 Jan 2018 11:56:02 -0800 Subject: [PATCH 069/128] Fix terminal links sometimes not working before scroll Fixes #36072 --- package.json | 2 +- yarn.lock | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 5720232afe8..965756d054b 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "vscode-debugprotocol": "1.25.0", "vscode-ripgrep": "^0.7.1-patch.0", "vscode-textmate": "^3.2.0", - "vscode-xterm": "3.1.0-beta6", + "vscode-xterm": "3.1.0-beta7", "yauzl": "2.8.0" }, "devDependencies": { diff --git a/yarn.lock b/yarn.lock index 50fc2a3bae6..91fc8617773 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5808,9 +5808,9 @@ vscode-textmate@^3.2.0: fast-plist "^0.1.2" oniguruma "^6.0.1" -vscode-xterm@3.1.0-beta6: - version "3.1.0-beta6" - resolved "https://registry.yarnpkg.com/vscode-xterm/-/vscode-xterm-3.1.0-beta6.tgz#0ff44249ac141e9f6dbcf0b7628d0b8d87e69abf" +vscode-xterm@3.1.0-beta7: + version "3.1.0-beta7" + resolved "https://registry.yarnpkg.com/vscode-xterm/-/vscode-xterm-3.1.0-beta7.tgz#10b0162baf8ddbf8454ba3ccb723c8808f8803af" vso-node-api@^6.1.2-preview: version "6.1.2-preview" From d7b6e7d7f684e07e7f24aa47b359bdfcb0cb6bc5 Mon Sep 17 00:00:00 2001 From: Rachel Macfarlane Date: Mon, 22 Jan 2018 11:53:35 -0800 Subject: [PATCH 070/128] No resizing of inputs in issue reporter, fixes #41992 --- src/vs/code/electron-browser/issue/issueReporterMain.ts | 2 +- src/vs/code/electron-browser/issue/media/issueReporter.css | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/vs/code/electron-browser/issue/issueReporterMain.ts b/src/vs/code/electron-browser/issue/issueReporterMain.ts index f37957e1518..898e52222bf 100644 --- a/src/vs/code/electron-browser/issue/issueReporterMain.ts +++ b/src/vs/code/electron-browser/issue/issueReporterMain.ts @@ -95,7 +95,7 @@ export class IssueReporter extends Disposable { if (styles.inputBorder) { content.push(`input, textarea, select { border: 1px solid ${styles.inputBorder}; }`); } else { - content.push(`input, textarea, select { border: none; }`); + content.push(`input, textarea, select { border: 1px solid transparent; }`); } if (styles.inputForeground) { diff --git a/src/vs/code/electron-browser/issue/media/issueReporter.css b/src/vs/code/electron-browser/issue/media/issueReporter.css index cb43afc9bc1..e4ca54e35b5 100644 --- a/src/vs/code/electron-browser/issue/media/issueReporter.css +++ b/src/vs/code/electron-browser/issue/media/issueReporter.css @@ -165,9 +165,14 @@ button:disabled { } select, input, textarea { + border: 1px solid transparent; margin-top: 10px; } +summary { + border: 1px solid transparent; +} + .validation-error { font-size: 12px; font-weight: bold; From 09c9d52e3f137b7de9b3fdf20c636ea074e9584a Mon Sep 17 00:00:00 2001 From: Peng Lyu Date: Mon, 22 Jan 2018 12:51:04 -0800 Subject: [PATCH 071/128] snapshot for empty file --- .../pieceTreeTextBuffer/pieceTreeBase.ts | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts b/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts index 9c4b974dd11..6af4001ded7 100644 --- a/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts +++ b/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts @@ -164,14 +164,27 @@ class PieceTreeSnapshot implements ITextSnapshot { this._nodes = []; this._tree = tree; this._BOM = BOM; - tree.iterate(tree.root, node => { - this._nodes.push(node); - return true; - }); this._index = 0; + if (tree.root !== SENTINEL) { + tree.iterate(tree.root, node => { + if (node !== SENTINEL) { + this._nodes.push(node); + } + return true; + }); + } } read(): string { + if (this._nodes.length === 0) { + if (this._index === 0) { + this._index++; + return this._BOM; + } else { + return null; + } + } + if (this._index > this._nodes.length - 1) { return null; } From 220f4b56cced5237690f85269a2740c07ffcc540 Mon Sep 17 00:00:00 2001 From: Peng Lyu Date: Mon, 22 Jan 2018 13:04:35 -0800 Subject: [PATCH 072/128] fix integration test save for untitle file --- .../editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts b/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts index 6af4001ded7..e9e428b9106 100644 --- a/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts +++ b/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts @@ -290,6 +290,9 @@ export class PieceTreeBase { let offset = 0; let ret = this.iterate(this.root, node => { + if (node === SENTINEL) { + return true; + } let str = this.getNodeContent(node); let len = str.length; let startPosition = other.nodeAt(offset); From fc691f719c46bd6ea923a82ccf3b5fbcface3f7e Mon Sep 17 00:00:00 2001 From: Rachel Macfarlane Date: Mon, 22 Jan 2018 13:33:44 -0800 Subject: [PATCH 073/128] Update issue reporter wording, fixes #41994 --- .../code/electron-browser/issue/issueReporter.html | 12 ++++++------ .../code/electron-browser/issue/issueReporterMain.ts | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/vs/code/electron-browser/issue/issueReporter.html b/src/vs/code/electron-browser/issue/issueReporter.html index 553c3d51de5..ad3010252ec 100644 --- a/src/vs/code/electron-browser/issue/issueReporter.html +++ b/src/vs/code/electron-browser/issue/issueReporter.html @@ -9,7 +9,7 @@

- +
@@ -42,7 +42,7 @@
My System Info - +
@@ -54,7 +54,7 @@
Currently Running Processes - +
@@ -66,7 +66,7 @@
My Workspace Stats - +
@@ -86,7 +86,7 @@
 					
We support GitHub-flavored Markdown. - After submitting, you will still be able to edit your issue on GitHub. + You will still be able to edit your issue when we preview it on GitHub. diff --git a/src/vs/code/electron-browser/issue/issueReporterMain.ts b/src/vs/code/electron-browser/issue/issueReporterMain.ts index 898e52222bf..c5300541bde 100644 --- a/src/vs/code/electron-browser/issue/issueReporterMain.ts +++ b/src/vs/code/electron-browser/issue/issueReporterMain.ts @@ -246,9 +246,9 @@ export class IssueReporter extends Disposable { hide(processBlock); hide(workspaceBlock); - descriptionTitle.innerHTML = 'Steps to reproduce *'; + descriptionTitle.innerHTML = 'Steps to Reproduce *'; show(descriptionSubtitle); - descriptionSubtitle.innerHTML = 'How did you encounter this problem? Clear steps to reproduce the problem help our investigation. What did you expect to happen and what actually happened?'; + descriptionSubtitle.innerHTML = 'How did you encounter this problem? Please provide clear steps to reproduce the problem during our investigation. What did you expect to happen and what actually did happen?'; } // 2 - Perf Issue else if (issueType === 1) { @@ -256,7 +256,7 @@ export class IssueReporter extends Disposable { show(processBlock); show(workspaceBlock); - descriptionTitle.innerHTML = 'Steps to reproduce *'; + descriptionTitle.innerHTML = 'Steps to Reproduce *'; show(descriptionSubtitle); descriptionSubtitle.innerHTML = 'When did this performance issue happen? For example, does it occur on startup or after a specific series of actions? Any details you can provide help our investigation.'; } From 62fd6577aaa351eba77528da91a683a54698a85c Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Mon, 22 Jan 2018 14:08:00 -0800 Subject: [PATCH 074/128] Use POST request for settings search --- .../electron-browser/preferencesSearch.ts | 70 ++++++++++++------- 1 file changed, 45 insertions(+), 25 deletions(-) diff --git a/src/vs/workbench/parts/preferences/electron-browser/preferencesSearch.ts b/src/vs/workbench/parts/preferences/electron-browser/preferencesSearch.ts index e1ed15f5bd7..5f138ef5e40 100644 --- a/src/vs/workbench/parts/preferences/electron-browser/preferencesSearch.ts +++ b/src/vs/workbench/parts/preferences/electron-browser/preferencesSearch.ts @@ -19,6 +19,7 @@ import { IRequestService } from 'vs/platform/request/node/request'; import { asJson } from 'vs/base/node/request'; import { Disposable } from 'vs/base/common/lifecycle'; import { IExtensionManagementService, LocalExtensionType, ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { ILogService } from 'vs/platform/log/common/log'; export interface IEndpointDetails { urlBase: string; @@ -120,6 +121,7 @@ export class RemoteSearchProvider implements ISearchProvider { constructor(filter: string, private endpoint: IEndpointDetails, private installedExtensions: TPromise, private newExtensionsOnly: boolean, @IEnvironmentService private environmentService: IEnvironmentService, @IRequestService private requestService: IRequestService, + @ILogService private logService: ILogService ) { this._filter = filter; @@ -168,9 +170,14 @@ export class RemoteSearchProvider implements ISearchProvider { private getSettingsFromBing(filter: string): TPromise { const start = Date.now(); - return this.prepareUrl(filter).then(url => { + return this.prepareRequest(filter).then(details => { + this.logService.debug(`Searching settings via ${details.url}`); + this.logService.debug(`Body: ${details.body}`); + return this.requestService.request({ - url, + type: 'POST', + url: details.url, + data: details.body, headers: { 'User-Agent': 'request', 'Content-Type': 'application/json; charset=utf-8', @@ -179,7 +186,7 @@ export class RemoteSearchProvider implements ISearchProvider { timeout: 5000 }).then(context => { if (context.res.statusCode >= 300) { - throw new Error(`${url} returned status code: ${context.res.statusCode}`); + throw new Error(`${details} returned status code: ${context.res.statusCode}`); } return asJson(context); @@ -201,7 +208,7 @@ export class RemoteSearchProvider implements ISearchProvider { }); return { - remoteUrl: url, + remoteUrl: details.url, // telemetry for filter text? duration, timestamp, scoredResults, @@ -223,7 +230,7 @@ export class RemoteSearchProvider implements ISearchProvider { }; } - private prepareUrl(query: string): TPromise { + private async prepareRequest(query: string): TPromise<{ url: string, body?: string }> { query = escapeSpecialChars(query); const boost = 10; const userQuery = `(${query})^${boost}`; @@ -237,16 +244,24 @@ export class RemoteSearchProvider implements ISearchProvider { const buildNumber = this.environmentService.settingsSearchBuildId; if (this.endpoint.key) { url += `${API_VERSION}&${QUERY_TYPE}`; - url += `&search=${encodedQuery}`; + } - if (this.newExtensionsOnly) { - return TPromise.wrap(url); - } else { - return this.getVersionAndExtensionFilters(buildNumber).then(filters => { - url += `&$filter=${filters.join(' or ')}`; - return url; - }); - } + const usePost = true; + if (usePost) { + const filters = this.newExtensionsOnly ? + [`diminish eq 'latest'`] : + await this.getVersionFilters(buildNumber); + + const filterStr = encodeURIComponent(filters.join(' or ')); + const body = JSON.stringify({ + query: encodedQuery, + filters: filterStr + }); + + return { + url, + body + }; } else { url += `query=${encodedQuery}`; @@ -255,20 +270,15 @@ export class RemoteSearchProvider implements ISearchProvider { } } - return TPromise.wrap(url); + return TPromise.wrap({ url }); } - private getVersionAndExtensionFilters(buildNumber?: number): TPromise { + private getVersionFilters(buildNumber?: number): TPromise { return this.installedExtensions.then(exts => { - const filters = exts.map(ext => { - const uuid = ext.identifier.uuid; - const versionString = ext.manifest.version - .split('.') - .map(versionPart => strings.pad(versionPart, 10)) - .join(''); - - return `(packageid eq '${uuid}' and startbuildno le '${versionString}' and endbuildno ge '${versionString}')`; - }); + // Only search extensions that contribute settings + const filters = exts + .filter(ext => ext.manifest.contributes && ext.manifest.contributes.configuration) + .map(ext => this.getExtensionFilter(ext)); if (buildNumber) { filters.push(`(packageid eq 'core' and startbuildno le '${buildNumber}' and endbuildno ge '${buildNumber}')`); @@ -277,6 +287,16 @@ export class RemoteSearchProvider implements ISearchProvider { return filters; }); } + + private getExtensionFilter(ext: ILocalExtension): string { + const uuid = ext.identifier.uuid; + const versionString = ext.manifest.version + .split('.') + .map(versionPart => strings.pad(versionPart, 10)) + .join(''); + + return `(packageid eq '${uuid}' and startbuildno le '${versionString}' and endbuildno ge '${versionString}')`; + } } const API_VERSION = 'api-version=2016-09-01-Preview'; From eefac3921e41a9c10d951a71b69e33ee661eb67b Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Mon, 22 Jan 2018 14:26:45 -0800 Subject: [PATCH 075/128] Also filter settings search by extension package id --- .../parts/preferences/common/preferences.ts | 3 +- .../preferences/common/preferencesModels.ts | 2 +- .../electron-browser/preferencesSearch.ts | 34 ++++++++++++------- 3 files changed, 25 insertions(+), 14 deletions(-) diff --git a/src/vs/workbench/parts/preferences/common/preferences.ts b/src/vs/workbench/parts/preferences/common/preferences.ts index ff7620dee0f..52694d377cd 100644 --- a/src/vs/workbench/parts/preferences/common/preferences.ts +++ b/src/vs/workbench/parts/preferences/common/preferences.ts @@ -89,6 +89,7 @@ export interface IScoredResults { export interface IRemoteSetting { score: number; key: string; + id: string; defaultValue: string; description: string; packageId: string; @@ -111,7 +112,7 @@ export interface IPreferencesEditorModel { } export type IGroupFilter = (group: ISettingsGroup) => boolean; -export type ISettingMatcher = (setting: ISetting) => { matches: IRange[], score: number }; +export type ISettingMatcher = (setting: ISetting, group: ISettingsGroup) => { matches: IRange[], score: number }; export interface ISettingsEditorModel extends IPreferencesEditorModel { readonly onDidChangeGroups: Event; diff --git a/src/vs/workbench/parts/preferences/common/preferencesModels.ts b/src/vs/workbench/parts/preferences/common/preferencesModels.ts index e9e5d435af2..959b05501d2 100644 --- a/src/vs/workbench/parts/preferences/common/preferencesModels.ts +++ b/src/vs/workbench/parts/preferences/common/preferencesModels.ts @@ -57,7 +57,7 @@ export abstract class AbstractSettingsModel extends EditorModel { const groupMatched = groupFilter(group); for (const section of group.sections) { for (const setting of section.settings) { - const settingMatchResult = settingMatcher(setting); + const settingMatchResult = settingMatcher(setting, group); if (groupMatched || settingMatchResult) { filterMatches.push({ diff --git a/src/vs/workbench/parts/preferences/electron-browser/preferencesSearch.ts b/src/vs/workbench/parts/preferences/electron-browser/preferencesSearch.ts index 5f138ef5e40..d91e3a967bf 100644 --- a/src/vs/workbench/parts/preferences/electron-browser/preferencesSearch.ts +++ b/src/vs/workbench/parts/preferences/electron-browser/preferencesSearch.ts @@ -20,6 +20,7 @@ import { asJson } from 'vs/base/node/request'; import { Disposable } from 'vs/base/common/lifecycle'; import { IExtensionManagementService, LocalExtensionType, ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ILogService } from 'vs/platform/log/common/log'; +import { IStringDictionary } from 'vs/base/common/collections'; export interface IEndpointDetails { urlBase: string; @@ -125,8 +126,7 @@ export class RemoteSearchProvider implements ISearchProvider { ) { this._filter = filter; - // @queries are always handled by local filter - this._remoteSearchP = filter && !strings.startsWith(filter, '@') ? + this._remoteSearchP = filter ? this.getSettingsFromBing(filter) : TPromise.wrap(null); } @@ -194,17 +194,23 @@ export class RemoteSearchProvider implements ISearchProvider { const timestamp = Date.now(); const duration = timestamp - start; const remoteSettings: IRemoteSetting[] = (result.value || []) - .map(r => ({ - key: JSON.parse(r.setting || r.Setting), - defaultValue: r['value'], - score: r['@search.score'], - description: JSON.parse(r['details']), - packageId: r['packageid'] - })); + .map(r => { + const key = JSON.parse(r.setting || r.Setting); + const packageId = r['packageid']; + const id = getSettingKey(packageId, key); + return { + key, + id, + defaultValue: r['value'], + score: r['@search.score'], + description: JSON.parse(r['details']), + packageId + }; + }); const scoredResults = Object.create(null); remoteSettings.forEach(s => { - scoredResults[s.key] = s; + scoredResults[s.id] = s; }); return { @@ -219,8 +225,8 @@ export class RemoteSearchProvider implements ISearchProvider { } private getRemoteSettingMatcher(scoredResults: IScoredResults, minScore: number, preferencesModel: ISettingsEditorModel): ISettingMatcher { - return (setting: ISetting) => { - const remoteSetting = scoredResults[setting.key]; + return (setting: ISetting, group: ISettingsGroup) => { + const remoteSetting = scoredResults[getSettingKey(group.id, setting.key)]; if (remoteSetting && remoteSetting.score >= minScore) { const settingMatches = new SettingMatches(this._filter, setting, false, (filter, setting) => preferencesModel.findValueMatches(filter, setting)).matches; return { matches: settingMatches, score: remoteSetting.score }; @@ -299,6 +305,10 @@ export class RemoteSearchProvider implements ISearchProvider { } } +function getSettingKey(packageId: string, name: string): string { + return packageId + '_' + name; +} + const API_VERSION = 'api-version=2016-09-01-Preview'; const QUERY_TYPE = 'querytype=full'; From 9081507e8930829ebc39cdc57589198ce5f9a08e Mon Sep 17 00:00:00 2001 From: Brian Schlenker Date: Mon, 22 Jan 2018 16:11:34 -0800 Subject: [PATCH 076/128] Add ability to zoom in/out on all images (#38538) * Add ability to zoom in on small images * Update image viewer to allow pinch or click to zoom Images are now always centered in the window. They initially start at their native size, unless they would be larger than the window, in which case they are contained within the window. Clicking increases the zoom, and alt+click decreases it. Pinch to zoom and ctrl+scroll are also supported. * Update resourceViewer to improve image viewing experience ResourceViewer now holds a cache of image scales so they stay the same while flipping between editor tabs. Right clicking now returns the image to its original scale. Pixelation only triggers for images 64x64 or smaller, and only after the first zoom. Editor risizing is handled thorugh the layout call to the binary editor, passed down to the resource viewer. --- .../ui/resourceviewer/resourceViewer.ts | 131 ++++++++++++++++-- .../ui/resourceviewer/resourceviewer.css | 19 ++- .../browser/parts/editor/binaryEditor.ts | 10 +- 3 files changed, 139 insertions(+), 21 deletions(-) diff --git a/src/vs/base/browser/ui/resourceviewer/resourceViewer.ts b/src/vs/base/browser/ui/resourceviewer/resourceViewer.ts index 9f1e41502d2..a1e5c3fcb89 100644 --- a/src/vs/base/browser/ui/resourceviewer/resourceViewer.ts +++ b/src/vs/base/browser/ui/resourceviewer/resourceViewer.ts @@ -10,11 +10,12 @@ import nls = require('vs/nls'); import mimes = require('vs/base/common/mime'); import URI from 'vs/base/common/uri'; import paths = require('vs/base/common/paths'); -import { Builder, $ } from 'vs/base/browser/builder'; +import { Builder, $, Dimension } from 'vs/base/browser/builder'; import DOM = require('vs/base/browser/dom'); import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; import { LRUCache } from 'vs/base/common/map'; import { Schemas } from 'vs/base/common/network'; +import { clamp } from 'vs/base/common/numbers'; interface MapExtToMediaMimes { [index: string]: string; @@ -78,6 +79,10 @@ export interface IResourceDescriptor { mime: string; } +enum ScaleDirection { + IN, OUT, +} + // Chrome is caching images very aggressively and so we use the ETag information to find out if // we need to bypass the cache or not. We could always bypass the cache everytime we show the image // however that has very bad impact on memory consumption because each time the image gets shown, @@ -104,6 +109,13 @@ function imageSrc(descriptor: IResourceDescriptor): string { return cached.src; } +// store the scale of an image so it can be restored when changing editor tabs +const IMAGE_SCALE_CACHE = new LRUCache(100); + +export interface ResourceViewerContext { + layout(dimension: Dimension); +} + /** * Helper to actually render the given resource into the provided container. Will adjust scrollbar (if provided) automatically based on loading * progress of the binary resource. @@ -117,13 +129,19 @@ export class ResourceViewer { private static readonly MAX_IMAGE_SIZE = ResourceViewer.MB; // showing images inline is memory intense, so we have a limit + private static SCALE_PINCH_FACTOR = 0.1; + private static SCALE_FACTOR = 1.5; + private static MAX_SCALE = 20; + private static MIN_SCALE = 0.1; + private static PIXELATION_THRESHOLD = 64; // enable image-rendering: pixelated for images less than this + public static show( descriptor: IResourceDescriptor, container: Builder, scrollbar: DomScrollableElement, openExternal: (uri: URI) => void, metadataClb?: (meta: string) => void - ): void { + ): ResourceViewerContext { // Ensure CSS class $(container).setClass('monaco-resource-viewer'); @@ -144,28 +162,115 @@ export class ResourceViewer { // Show Image inline unless they are large if (mime.indexOf('image/') >= 0) { if (ResourceViewer.inlineImage(descriptor)) { + const context = { + layout(dimension: Dimension) { } + }; $(container) .empty() - .addClass('image') + .addClass('image', 'zoom-in') .img({ src: imageSrc(descriptor) }) + .addClass('untouched') .on(DOM.EventType.LOAD, (e, img) => { const imgElement = img.getHTMLElement(); - if (imgElement.naturalWidth > imgElement.width || imgElement.naturalHeight > imgElement.height) { - $(container).addClass('oversized'); + const cacheKey = descriptor.resource.toString(); + let scaleDirection = ScaleDirection.IN; + let scale = IMAGE_SCALE_CACHE.get(cacheKey) || null; + if (scale) { + img.removeClass('untouched'); + updateScale(scale); + } - img.on(DOM.EventType.CLICK, (e, img) => { - $(container).toggleClass('full-size'); + function setImageWidth(width) { + img.style('width', `${width}px`); + img.style('height', 'auto'); + } - scrollbar.scanDomNode(); + function updateScale(newScale) { + scale = clamp(newScale, ResourceViewer.MIN_SCALE, ResourceViewer.MAX_SCALE); + setImageWidth(Math.floor(imgElement.naturalWidth * scale)); + IMAGE_SCALE_CACHE.set(cacheKey, scale); + + scrollbar.scanDomNode(); + + updateMetadata(); + } + + function updateMetadata() { + if (metadataClb) { + const scale = Math.round((imgElement.width / imgElement.naturalWidth) * 10000) / 100; + metadataClb(nls.localize('imgMeta', '{0}% {1}x{2} {3}', + scale, + imgElement.naturalWidth, + imgElement.naturalHeight, + ResourceViewer.formatSize(descriptor.size))); + } + } + + context.layout = updateMetadata; + + function firstZoom() { + const { clientWidth, naturalWidth } = imgElement; + setImageWidth(clientWidth); + img.removeClass('untouched'); + if (imgElement.naturalWidth < ResourceViewer.PIXELATION_THRESHOLD + || imgElement.naturalHeight < ResourceViewer.PIXELATION_THRESHOLD) { + img.addClass('pixelated'); + } + scale = clientWidth / naturalWidth; + } + + $(container) + .on(DOM.EventType.KEY_DOWN, (e: KeyboardEvent, c) => { + if (e.altKey) { + scaleDirection = ScaleDirection.OUT; + c.removeClass('zoom-in').addClass('zoom-out'); + } + }) + .on(DOM.EventType.KEY_UP, (e: KeyboardEvent, c) => { + if (!e.altKey) { + scaleDirection = ScaleDirection.IN; + c.removeClass('zoom-out').addClass('zoom-in'); + } }); - } - if (metadataClb) { - metadataClb(nls.localize('imgMeta', "{0}x{1} {2}", imgElement.naturalWidth, imgElement.naturalHeight, ResourceViewer.formatSize(descriptor.size))); - } + $(container).on(DOM.EventType.MOUSE_DOWN, (e: MouseEvent) => { + if (scale === null) { + firstZoom(); + } + + // right click + if (e.button === 2) { + updateScale(1); + } else { + const scaleFactor = scaleDirection === ScaleDirection.IN + ? ResourceViewer.SCALE_FACTOR + : 1 / ResourceViewer.SCALE_FACTOR; + + updateScale(scale * scaleFactor); + } + }); + + $(container).on(DOM.EventType.WHEEL, (e: WheelEvent) => { + // pinching is reported as scroll wheel + ctrl + if (!e.ctrlKey) { + return; + } + + if (scale === null) { + firstZoom(); + } + + // scrolling up, pinching out should increase the scale + const delta = -e.deltaY; + updateScale(scale + delta * ResourceViewer.SCALE_PINCH_FACTOR); + }); + + updateMetadata(); scrollbar.scanDomNode(); }); + + return context; } else { const imageContainer = $(container) .empty() @@ -199,6 +304,8 @@ export class ResourceViewer { scrollbar.scanDomNode(); } + + return null; } private static inlineImage(descriptor: IResourceDescriptor): boolean { diff --git a/src/vs/base/browser/ui/resourceviewer/resourceviewer.css b/src/vs/base/browser/ui/resourceviewer/resourceviewer.css index c4badd1d2c5..98110688e66 100644 --- a/src/vs/base/browser/ui/resourceviewer/resourceviewer.css +++ b/src/vs/base/browser/ui/resourceviewer/resourceviewer.css @@ -16,6 +16,7 @@ padding: 10px 10px 0 10px; background-position: 0 0, 8px 8px; background-size: 16px 16px; + display: grid; } .monaco-resource-viewer.image.full-size { @@ -34,18 +35,24 @@ linear-gradient(45deg, rgb(20, 20, 20) 25%, transparent 25%, transparent 75%, rgb(20, 20, 20) 75%, rgb(20, 20, 20)); } -.monaco-resource-viewer img { +.monaco-resource-viewer img.untouched { max-width: 100%; - max-height: calc(100% - 10px); /* somehow this prevents scrollbars from showing up */ + object-fit: contain; } -.monaco-resource-viewer.oversized img { +.monaco-resource-viewer img.pixelated { + image-rendering: pixelated; +} + +.monaco-resource-viewer img { + margin: auto; /* centers the image */ +} + +.monaco-resource-viewer.zoom-in { cursor: zoom-in; } -.monaco-resource-viewer.full-size img { - max-width: initial; - max-height: initial; +.monaco-resource-viewer.zoom-out { cursor: zoom-out; } diff --git a/src/vs/workbench/browser/parts/editor/binaryEditor.ts b/src/vs/workbench/browser/parts/editor/binaryEditor.ts index 282705c8e74..8d4b64f9f4b 100644 --- a/src/vs/workbench/browser/parts/editor/binaryEditor.ts +++ b/src/vs/workbench/browser/parts/editor/binaryEditor.ts @@ -10,7 +10,7 @@ import Event, { Emitter } from 'vs/base/common/event'; import URI from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import { Dimension, Builder, $ } from 'vs/base/browser/builder'; -import { ResourceViewer } from 'vs/base/browser/ui/resourceviewer/resourceViewer'; +import { ResourceViewer, ResourceViewerContext } from 'vs/base/browser/ui/resourceviewer/resourceViewer'; import { EditorModel, EditorInput, EditorOptions } from 'vs/workbench/common/editor'; import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; import { BinaryEditorModel } from 'vs/workbench/common/editor/binaryEditorModel'; @@ -29,6 +29,7 @@ export abstract class BaseBinaryResourceEditor extends BaseEditor { private binaryContainer: Builder; private scrollbar: DomScrollableElement; + private resourceViewerContext: ResourceViewerContext; constructor( id: string, @@ -87,7 +88,7 @@ export abstract class BaseBinaryResourceEditor extends BaseEditor { // Render Input const model = resolvedModel; - ResourceViewer.show( + this.resourceViewerContext = ResourceViewer.show( { name: model.getName(), resource: model.getResource(), size: model.getSize(), etag: model.getETag(), mime: model.getMime() }, this.binaryContainer, this.scrollbar, @@ -132,6 +133,9 @@ export abstract class BaseBinaryResourceEditor extends BaseEditor { // Pass on to Binary Container this.binaryContainer.size(dimension.width, dimension.height); this.scrollbar.scanDomNode(); + if (this.resourceViewerContext) { + this.resourceViewerContext.layout(dimension); + } } public focus(): void { @@ -146,4 +150,4 @@ export abstract class BaseBinaryResourceEditor extends BaseEditor { super.dispose(); } -} \ No newline at end of file +} From 3fc5ab42da1d81167a3a467608914693f0e53399 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Mon, 22 Jan 2018 14:43:09 -0800 Subject: [PATCH 077/128] Use flatten --- .../goToDeclaration/goToDeclaration.ts | 21 +++++-------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/src/vs/editor/contrib/goToDeclaration/goToDeclaration.ts b/src/vs/editor/contrib/goToDeclaration/goToDeclaration.ts index b3ecb2c6d4b..f79912d8323 100644 --- a/src/vs/editor/contrib/goToDeclaration/goToDeclaration.ts +++ b/src/vs/editor/contrib/goToDeclaration/goToDeclaration.ts @@ -14,20 +14,7 @@ import { DefinitionProviderRegistry, ImplementationProviderRegistry, TypeDefinit import { CancellationToken } from 'vs/base/common/cancellation'; import { asWinJsPromise } from 'vs/base/common/async'; import { Position } from 'vs/editor/common/core/position'; - -function outputResults(promises: TPromise[]) { - return TPromise.join(promises).then(allReferences => { - let result: Location[] = []; - for (let references of allReferences) { - if (Array.isArray(references)) { - result.push(...references); - } else if (references) { - result.push(references); - } - } - return result; - }); -} +import { flatten } from 'vs/base/common/arrays'; function getDefinitions( model: ITextModel, @@ -38,7 +25,7 @@ function getDefinitions( const provider = registry.ordered(model); // get results - const promises = provider.map((provider, idx) => { + const promises = provider.map((provider, idx): TPromise => { return asWinJsPromise((token) => { return provide(provider, model, position, token); }).then(undefined, err => { @@ -46,7 +33,9 @@ function getDefinitions( return null; }); }); - return outputResults(promises); + return TPromise.join(promises) + .then(flatten) + .then(references => references.filter(x => !!x)); } From 5d397568b566fe11e78ba59cb28b16f191041cd9 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Mon, 22 Jan 2018 16:18:32 -0800 Subject: [PATCH 078/128] Add refactor editor action Adds a new refactor editor action. This action shows a lightbulb context menu with only code actions of kind `refactor.*` --- .../contrib/quickFix/codeActionTrigger.ts | 1 + .../contrib/quickFix/quickFixCommands.ts | 24 +++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/src/vs/editor/contrib/quickFix/codeActionTrigger.ts b/src/vs/editor/contrib/quickFix/codeActionTrigger.ts index 0cd81d55df8..a210e484446 100644 --- a/src/vs/editor/contrib/quickFix/codeActionTrigger.ts +++ b/src/vs/editor/contrib/quickFix/codeActionTrigger.ts @@ -9,6 +9,7 @@ export class CodeActionKind { private static readonly sep = '.'; public static readonly Empty = new CodeActionKind(''); + public static readonly Refactor = new CodeActionKind('refactor'); constructor( public readonly value: string diff --git a/src/vs/editor/contrib/quickFix/quickFixCommands.ts b/src/vs/editor/contrib/quickFix/quickFixCommands.ts index 9850c794400..15f8c5ee8d8 100644 --- a/src/vs/editor/contrib/quickFix/quickFixCommands.ts +++ b/src/vs/editor/contrib/quickFix/quickFixCommands.ts @@ -222,6 +222,30 @@ export class CodeActionCommand extends EditorCommand { } } + +export class RefactorAction extends EditorAction { + + static readonly Id = 'editor.action.refactor'; + + constructor() { + super({ + id: RefactorAction.Id, + label: nls.localize('refactor.label', "Refactor"), + alias: 'Refactor', + precondition: ContextKeyExpr.and(EditorContextKeys.writable, EditorContextKeys.hasCodeActionsProvider) + }); + } + + public run(accessor: ServicesAccessor, editor: ICodeEditor): void { + const controller = QuickFixController.get(editor); + if (controller) { + controller.triggerCodeActionFromEditorSelection(CodeActionKind.Refactor, CodeActionAutoApply.Never); + } + } +} + + registerEditorContribution(QuickFixController); registerEditorAction(QuickFixAction); +registerEditorAction(RefactorAction); registerEditorCommand(new CodeActionCommand()); From c09f39ff8094bb5079cc3b2765f4b76c5c63dfba Mon Sep 17 00:00:00 2001 From: Rachel Macfarlane Date: Mon, 22 Jan 2018 16:20:52 -0800 Subject: [PATCH 079/128] Issue Reporter: Preview on 'Shift Enter', fixes #41996 --- .../electron-browser/issue/issueReporterMain.ts | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/vs/code/electron-browser/issue/issueReporterMain.ts b/src/vs/code/electron-browser/issue/issueReporterMain.ts index c5300541bde..0b8148580cc 100644 --- a/src/vs/code/electron-browser/issue/issueReporterMain.ts +++ b/src/vs/code/electron-browser/issue/issueReporterMain.ts @@ -6,7 +6,7 @@ 'use strict'; import 'vs/css!./media/issueReporter'; -import { shell, ipcRenderer, webFrame } from 'electron'; +import { shell, ipcRenderer, webFrame, remote } from 'electron'; import { $ } from 'vs/base/browser/dom'; import * as browser from 'vs/base/browser/browser'; import product from 'vs/platform/node/product'; @@ -228,6 +228,15 @@ export class IssueReporter extends Disposable { }); document.getElementById('github-submit-btn').addEventListener('click', () => this.createIssue()); + + document.onkeydown = (e: KeyboardEvent) => { + if (e.shiftKey && e.keyCode === 13) { + // Close the window if the issue was successfully created + if (this.createIssue()) { + remote.getCurrentWindow().close(); + } + } + }; } private renderBlocks(): void { @@ -294,7 +303,7 @@ export class IssueReporter extends Disposable { return isValid; } - private createIssue(): void { + private createIssue(): boolean { if (!this.validateInputs()) { // If inputs are invalid, set focus to the first one and add listeners on them // to detect further changes @@ -307,7 +316,8 @@ export class IssueReporter extends Disposable { document.getElementById('description').addEventListener('input', (event) => { this.validateInput('description'); }); - return; + + return false; } if (this.telemetryService) { @@ -323,6 +333,7 @@ export class IssueReporter extends Disposable { const baseUrl = `https://github.com/microsoft/vscode/issues/new?title=${issueTitle}&body=`; const issueBody = this.issueReporterModel.serialize(); shell.openExternal(baseUrl + encodeURIComponent(issueBody)); + return true; } /** From 20cc7d1957a8b6c445a6ee8bc87ed649a24c0ebe Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Mon, 22 Jan 2018 17:10:23 -0800 Subject: [PATCH 080/128] Show codelenses for new extension search results --- .../preferences/browser/preferencesEditor.ts | 25 +++--- .../browser/preferencesRenderers.ts | 89 +++++++++---------- .../parts/preferences/common/preferences.ts | 7 ++ .../preferences/common/preferencesModels.ts | 44 ++++----- .../electron-browser/preferencesSearch.ts | 20 ++++- 5 files changed, 102 insertions(+), 83 deletions(-) diff --git a/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts b/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts index d8f1a411ebf..158b44e088b 100644 --- a/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts +++ b/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts @@ -366,7 +366,7 @@ class PreferencesRenderersController extends Disposable { private _editablePreferencesRendererDisposables: IDisposable[] = []; private _settingsNavigator: SettingsNavigator; - private _remoteFiltersInProgress: TPromise[]; + private _remoteFilterInProgress: TPromise; private _currentLocalSearchProvider: ISearchProvider; private _currentRemoteSearchProvider: ISearchProvider; @@ -427,21 +427,19 @@ class PreferencesRenderersController extends Disposable { } remoteSearchPreferences(query: string, updateCurrentResults?: boolean): TPromise { - if (this._remoteFiltersInProgress) { + if (this._remoteFilterInProgress && this._remoteFilterInProgress.cancel) { // Resolved/rejected promises have no .cancel() - this._remoteFiltersInProgress.forEach(p => p.cancel && p.cancel()); + this._remoteFilterInProgress.cancel(); } this._currentRemoteSearchProvider = (updateCurrentResults && this._currentRemoteSearchProvider) || this.preferencesSearchService.getRemoteSearchProvider(query); this._currentNewExtensionsSearchProvider = (updateCurrentResults && this._currentNewExtensionsSearchProvider) || this.preferencesSearchService.getRemoteSearchProvider(query, true); - this._remoteFiltersInProgress = [ - this.filterOrSearchPreferences(query, this._currentRemoteSearchProvider, 'nlpResult', nls.localize('nlpResult', "Natural Language Results"), 1), - this.filterOrSearchPreferences(query, this._currentNewExtensionsSearchProvider, 'newExtensionsResult', nls.localize('newExtensionsResult', "Other Extension Results"), 2) - ]; + this._remoteFilterInProgress = this.filterOrSearchPreferences(query, this._currentRemoteSearchProvider, 'nlpResult', nls.localize('nlpResult', "Natural Language Results"), 1) + .then(result => this.filterOrSearchPreferences(query, this._currentNewExtensionsSearchProvider, 'newExtensionsResult', nls.localize('newExtensionsResult', "Other Extension Results"), 2)); - return TPromise.join(this._remoteFiltersInProgress).then(() => { - this._remoteFiltersInProgress = null; + return this._remoteFilterInProgress.then(() => { + this._remoteFilterInProgress = null; }, err => { if (isPromiseCanceledError(err)) { return null; @@ -456,13 +454,12 @@ class PreferencesRenderersController extends Disposable { return this.filterOrSearchPreferences(query, this._currentLocalSearchProvider, 'filterResult', nls.localize('filterResult', "Filtered Results"), 0); } - private filterOrSearchPreferences(query: string, searchProvider: ISearchProvider, groupId: string, groupLabel: string, groupOrder: number, newExtensionsOnly?: boolean): TPromise { + private filterOrSearchPreferences(query: string, searchProvider: ISearchProvider, groupId: string, groupLabel: string, groupOrder: number): TPromise { this._lastQuery = query; - const filterPs = [this._filterOrSearchPreferences(query, this.defaultPreferencesRenderer, searchProvider, groupId, groupLabel, groupOrder)]; - if (!newExtensionsOnly) { - filterPs.push(this._filterOrSearchPreferences(query, this.defaultPreferencesRenderer, searchProvider, groupId, groupLabel, groupOrder)); - } + const filterPs = [ + this._filterOrSearchPreferences(query, this.defaultPreferencesRenderer, searchProvider, groupId, groupLabel, groupOrder), + this._filterOrSearchPreferences(query, this.editablePreferencesRenderer, searchProvider, groupId, groupLabel, groupOrder)]; return TPromise.join(filterPs).then(results => { const [defaultFilterResult, editableFilterResult] = results; diff --git a/src/vs/workbench/parts/preferences/browser/preferencesRenderers.ts b/src/vs/workbench/parts/preferences/browser/preferencesRenderers.ts index edbd73470a7..c36f7249579 100644 --- a/src/vs/workbench/parts/preferences/browser/preferencesRenderers.ts +++ b/src/vs/workbench/parts/preferences/browser/preferencesRenderers.ts @@ -17,7 +17,7 @@ import * as editorCommon from 'vs/editor/common/editorCommon'; import { Range, IRange } from 'vs/editor/common/core/range'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope, IConfigurationPropertySchema } from 'vs/platform/configuration/common/configurationRegistry'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IPreferencesService, ISettingsGroup, ISetting, IPreferencesEditorModel, IFilterResult, ISettingsEditorModel, IScoredResults, IWorkbenchSettingsConfiguration } from 'vs/workbench/parts/preferences/common/preferences'; +import { IPreferencesService, ISettingsGroup, ISetting, IPreferencesEditorModel, IFilterResult, ISettingsEditorModel, IScoredResults, IWorkbenchSettingsConfiguration, IRemoteSetting, IExtensionSetting } from 'vs/workbench/parts/preferences/common/preferences'; import { SettingsEditorModel, DefaultSettingsEditorModel, WorkspaceConfigurationEditorModel } from 'vs/workbench/parts/preferences/common/preferencesModels'; import { ICodeEditor, IEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser'; import { IContextMenuService, ContextSubMenu } from 'vs/platform/contextview/browser/contextView'; @@ -34,7 +34,8 @@ import { MarkdownString } from 'vs/base/common/htmlContent'; import { overrideIdentifierFromKey, IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { ITextModel, IModelDeltaDecoration, TrackedRangeStickiness } from 'vs/editor/common/model'; -import { ICommandService } from 'vs/platform/commands/common/commands'; +import { CodeLensProviderRegistry, CodeLensProvider, ICodeLensSymbol } from 'vs/editor/common/modes'; +import { CancellationToken } from 'vs/base/common/cancellation'; export interface IPreferencesRenderer extends IDisposable { readonly preferencesModel: IPreferencesEditorModel; @@ -310,6 +311,7 @@ export class DefaultSettingsRenderer extends Disposable implements IPreferencesR public filterPreferences(filterResult: IFilterResult): void { this.filterResult = filterResult; + if (filterResult) { this.filteredMatchesRenderer.render(filterResult, this.preferencesModel.settingsGroups); this.settingsGroupTitleRenderer.render(filterResult.filteredGroups); @@ -622,20 +624,23 @@ export class FeedbackWidgetRenderer extends Disposable { const result = this._currentResult; const actualResults = result.metadata.scoredResults; - const actualResultNames = Object.keys(actualResults); + const actualResultIds = Object.keys(actualResults); const feedbackQuery: any = {}; feedbackQuery['comment'] = FeedbackWidgetRenderer.DEFAULT_COMMENT_TEXT; feedbackQuery['queryString'] = result.query; feedbackQuery['resultScores'] = {}; - actualResultNames.forEach(settingKey => { - feedbackQuery['resultScores'][settingKey] = 10; + actualResultIds.forEach(settingId => { + const outputKey = actualResults[settingId].key; + feedbackQuery['resultScores'][outputKey] = 10; }); feedbackQuery['alts'] = []; const contents = FeedbackWidgetRenderer.INSTRUCTION_TEXT + '\n' + JSON.stringify(feedbackQuery, undefined, ' ') + '\n\n' + - actualResultNames.map(name => `// ${name}: ${result.metadata.scoredResults[name]}`).join('\n'); + actualResultIds.map(name => { + return `// ${actualResults[name].key}: ${actualResults[name].score}`; + }).join('\n'); this.editorService.openEditor({ contents, language: 'jsonc' }, /*sideBySide=*/true).then(feedbackEditor => { const sendFeedbackWidget = this._register(this.instantiationService.createInstance(FloatingClickWidget, feedbackEditor.getControl(), 'Send feedback', null)); @@ -842,54 +847,48 @@ export class HighlightMatchesRenderer extends Disposable { } } -export class ExtensionCodelensRenderer extends Disposable { - private decorationIds: string[] = []; +export class ExtensionCodelensRenderer extends Disposable implements CodeLensProvider { + private filterResult: IFilterResult; - constructor(private editor: ICodeEditor, - @ICommandService private commandService: ICommandService) { + constructor() { super(); + this._register(CodeLensProviderRegistry.register({ pattern: '**/settings.json' }, this)); } public render(filterResult: IFilterResult): void { - this.editor.changeDecorations(changeAccessor => { - this.decorationIds = changeAccessor.deltaDecorations(this.decorationIds, []); - }); - - const newExtensionGroup = filterResult && arrays.first(filterResult.filteredGroups, g => g.id === 'newExtensionsResult'); - if (newExtensionGroup) { - this.editor.changeDecorations(changeAccessor => { - const settings = newExtensionGroup.sections[0].settings; - this.decorationIds = changeAccessor.deltaDecorations(this.decorationIds, settings.map(setting => this.createDecoration(setting))); - }); - } - - this._register(this.editor.onMouseDown((e: IEditorMouseEvent) => { - if (e.target.type !== MouseTargetType.GUTTER_GLYPH_MARGIN) { - return; - } - - this.commandService.executeCommand('workbench.extensions.action.showExtensionsWithId', 'ms-python.python'); - })); + this.filterResult = filterResult; } - private createDecoration(setting: ISetting): IModelDeltaDecoration { - return { - range: new Range(setting.keyRange.startLineNumber, 1, setting.keyRange.endLineNumber, 1), - options: { - glyphMarginClassName: 'newExtensionInstall', - stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, - } - }; - } - - public dispose() { - if (this.decorationIds) { - this.decorationIds = this.editor.changeDecorations(changeAccessor => { - return changeAccessor.deltaDecorations(this.decorationIds, []); - }); + public provideCodeLenses(model: ITextModel, token: CancellationToken): ICodeLensSymbol[] { + if (!this.filterResult || !this.filterResult.filteredGroups) { + return []; } - super.dispose(); + const newExtensionGroup = arrays.first(this.filterResult.filteredGroups, g => g.id === 'newExtensionsResult'); + if (!newExtensionGroup) { + return []; + } + + return newExtensionGroup.sections[0].settings + .filter((s: IExtensionSetting) => { + // Skip any non IExtensionSettings that somehow got in here + return s.extensionName && s.extensionPublisher; + }) + .map((s: IExtensionSetting) => { + const extId = s.extensionPublisher + '.' + s.extensionName; + return { + command: { + title: nls.localize('newExtensionLabel', "View \"{0}\"", extId), + id: 'workbench.extensions.action.showExtensionsWithId', + arguments: [extId.toLowerCase()] + }, + range: new Range(s.keyRange.startLineNumber, 1, s.keyRange.startLineNumber, 1) + }; + }); + } + + public resolveCodeLens(model: ITextModel, codeLens: ICodeLensSymbol, token: CancellationToken): ICodeLensSymbol { + return codeLens; } } diff --git a/src/vs/workbench/parts/preferences/common/preferences.ts b/src/vs/workbench/parts/preferences/common/preferences.ts index 52694d377cd..fc0212457ab 100644 --- a/src/vs/workbench/parts/preferences/common/preferences.ts +++ b/src/vs/workbench/parts/preferences/common/preferences.ts @@ -56,6 +56,11 @@ export interface ISetting { overrideOf?: ISetting; } +export interface IExtensionSetting extends ISetting { + extensionName: string; + extensionPublisher: string; +} + export interface ISearchResult { filterMatches: ISettingMatch[]; metadata?: IFilterMetadata; @@ -93,6 +98,8 @@ export interface IRemoteSetting { defaultValue: string; description: string; packageId: string; + extensionName?: string; + extensionPublisher?: string; } export interface IFilterMetadata { diff --git a/src/vs/workbench/parts/preferences/common/preferencesModels.ts b/src/vs/workbench/parts/preferences/common/preferencesModels.ts index 959b05501d2..044268407ba 100644 --- a/src/vs/workbench/parts/preferences/common/preferencesModels.ts +++ b/src/vs/workbench/parts/preferences/common/preferencesModels.ts @@ -41,12 +41,14 @@ export abstract class AbstractSettingsModel extends EditorModel { * Remove duplicates between result groups, preferring results in earlier groups */ private removeDuplicateResults(): void { - // Depends on order of map keys const settingKeys = new Set(); - this._currentResultGroups.forEach((group, id) => { - group.result.filterMatches = group.result.filterMatches.filter(s => !settingKeys.has(s.setting.key)); - group.result.filterMatches.forEach(s => settingKeys.add(s.setting.key)); - }); + map.keys(this._currentResultGroups) + .sort((a, b) => this._currentResultGroups.get(a).order - this._currentResultGroups.get(b).order) + .forEach(groupId => { + const group = this._currentResultGroups.get(groupId); + group.result.filterMatches = group.result.filterMatches.filter(s => !settingKeys.has(s.setting.key)); + group.result.filterMatches.forEach(s => settingKeys.add(s.setting.key)); + }); } public filterSettings(filter: string, groupFilter: IGroupFilter, settingMatcher: ISettingMatcher): ISettingMatch[] { @@ -61,7 +63,7 @@ export abstract class AbstractSettingsModel extends EditorModel { if (groupMatched || settingMatchResult) { filterMatches.push({ - setting, + setting: this.copySetting(setting), matches: settingMatchResult && settingMatchResult.matches, score: settingMatchResult ? settingMatchResult.score : 0 }); @@ -86,6 +88,21 @@ export abstract class AbstractSettingsModel extends EditorModel { return null; } + private copySetting(setting: ISetting): ISetting { + return { + description: setting.description, + descriptionRanges: setting.descriptionRanges, + key: setting.key, + keyRange: setting.keyRange, + value: setting.value, + range: setting.range, + valueRange: setting.valueRange, + overrides: [], + overrideOf: setting.overrideOf + }; + } + + protected get filterGroups(): ISettingsGroup[] { return this.settingsGroups; } @@ -720,19 +737,6 @@ export class DefaultSettingsEditorModel extends AbstractSettingsModel implements return null; } - private copySettings(settings: ISetting[]): ISetting[] { - return settings.map(setting => { - return { - description: setting.description, - key: setting.key, - value: setting.value, - range: null, - valueRange: null, - overrides: [] - }; - }); - } - private getGroup(resultGroup: ISearchResultGroup): ISettingsGroup { return { id: resultGroup.id, @@ -741,7 +745,7 @@ export class DefaultSettingsEditorModel extends AbstractSettingsModel implements titleRange: null, sections: [ { - settings: this.copySettings(resultGroup.result.filterMatches.map(m => m.setting)) + settings: resultGroup.result.filterMatches.map(m => m.setting) } ] }; diff --git a/src/vs/workbench/parts/preferences/electron-browser/preferencesSearch.ts b/src/vs/workbench/parts/preferences/electron-browser/preferencesSearch.ts index d91e3a967bf..1c4fe18c3a3 100644 --- a/src/vs/workbench/parts/preferences/electron-browser/preferencesSearch.ts +++ b/src/vs/workbench/parts/preferences/electron-browser/preferencesSearch.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { TPromise } from 'vs/base/common/winjs.base'; -import { ISettingsEditorModel, ISetting, ISettingsGroup, IWorkbenchSettingsConfiguration, IFilterMetadata, IPreferencesSearchService, ISearchResult, ISearchProvider, IGroupFilter, ISettingMatcher, IScoredResults, ISettingMatch, IRemoteSetting } from 'vs/workbench/parts/preferences/common/preferences'; +import { ISettingsEditorModel, ISetting, ISettingsGroup, IWorkbenchSettingsConfiguration, IFilterMetadata, IPreferencesSearchService, ISearchResult, ISearchProvider, IGroupFilter, ISettingMatcher, IScoredResults, ISettingMatch, IRemoteSetting, IExtensionSetting } from 'vs/workbench/parts/preferences/common/preferences'; import { IRange } from 'vs/editor/common/core/range'; import { distinct, top } from 'vs/base/common/arrays'; import * as strings from 'vs/base/common/strings'; @@ -198,13 +198,23 @@ export class RemoteSearchProvider implements ISearchProvider { const key = JSON.parse(r.setting || r.Setting); const packageId = r['packageid']; const id = getSettingKey(packageId, key); + + const packageName = r['packagename']; + let extensionName: string; + let extensionPublisher: string; + if (packageName.indexOf('##') >= 0) { + [extensionPublisher, extensionName] = packageName.split('##'); + } + return { key, id, defaultValue: r['value'], score: r['@search.score'], description: JSON.parse(r['details']), - packageId + packageId, + extensionName, + extensionPublisher }; }); @@ -319,7 +329,7 @@ function escapeSpecialChars(query: string): string { .trim(); } -function remoteSettingToISetting(remoteSetting: IRemoteSetting): ISetting { +function remoteSettingToISetting(remoteSetting: IRemoteSetting): IExtensionSetting { return { description: remoteSetting.description.split('\n'), descriptionRanges: null, @@ -328,7 +338,9 @@ function remoteSettingToISetting(remoteSetting: IRemoteSetting): ISetting { value: remoteSetting.defaultValue, range: null, valueRange: null, - overrides: [] + overrides: [], + extensionName: remoteSetting.extensionName, + extensionPublisher: remoteSetting.extensionPublisher }; } From 89cdbe2ad8d484b31e5d1db1056428b2408ed54d Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Mon, 22 Jan 2018 16:27:18 -0800 Subject: [PATCH 081/128] Mark static constants readonly --- .../base/browser/ui/resourceviewer/resourceViewer.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/vs/base/browser/ui/resourceviewer/resourceViewer.ts b/src/vs/base/browser/ui/resourceviewer/resourceViewer.ts index a1e5c3fcb89..1a05cc207c6 100644 --- a/src/vs/base/browser/ui/resourceviewer/resourceViewer.ts +++ b/src/vs/base/browser/ui/resourceviewer/resourceViewer.ts @@ -129,11 +129,11 @@ export class ResourceViewer { private static readonly MAX_IMAGE_SIZE = ResourceViewer.MB; // showing images inline is memory intense, so we have a limit - private static SCALE_PINCH_FACTOR = 0.1; - private static SCALE_FACTOR = 1.5; - private static MAX_SCALE = 20; - private static MIN_SCALE = 0.1; - private static PIXELATION_THRESHOLD = 64; // enable image-rendering: pixelated for images less than this + private static readonly SCALE_PINCH_FACTOR = 0.1; + private static readonly SCALE_FACTOR = 1.5; + private static readonly MAX_SCALE = 20; + private static readonly MIN_SCALE = 0.1; + private static readonly PIXELATION_THRESHOLD = 64; // enable image-rendering: pixelated for images less than this public static show( descriptor: IResourceDescriptor, From 1b21617a802d079877f7534d202ef7917a5576af Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Mon, 22 Jan 2018 16:31:16 -0800 Subject: [PATCH 082/128] Make sure we preserve pixelated property on zoomed images --- .../base/browser/ui/resourceviewer/resourceViewer.ts | 10 ++++++---- .../base/browser/ui/resourceviewer/resourceviewer.css | 9 +++++---- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/vs/base/browser/ui/resourceviewer/resourceViewer.ts b/src/vs/base/browser/ui/resourceviewer/resourceViewer.ts index 1a05cc207c6..e1901ab0748 100644 --- a/src/vs/base/browser/ui/resourceviewer/resourceViewer.ts +++ b/src/vs/base/browser/ui/resourceviewer/resourceViewer.ts @@ -180,6 +180,12 @@ export class ResourceViewer { updateScale(scale); } + if (imgElement.naturalWidth < ResourceViewer.PIXELATION_THRESHOLD + || imgElement.naturalHeight < ResourceViewer.PIXELATION_THRESHOLD + ) { + img.addClass('pixelated'); + } + function setImageWidth(width) { img.style('width', `${width}px`); img.style('height', 'auto'); @@ -212,10 +218,6 @@ export class ResourceViewer { const { clientWidth, naturalWidth } = imgElement; setImageWidth(clientWidth); img.removeClass('untouched'); - if (imgElement.naturalWidth < ResourceViewer.PIXELATION_THRESHOLD - || imgElement.naturalHeight < ResourceViewer.PIXELATION_THRESHOLD) { - img.addClass('pixelated'); - } scale = clientWidth / naturalWidth; } diff --git a/src/vs/base/browser/ui/resourceviewer/resourceviewer.css b/src/vs/base/browser/ui/resourceviewer/resourceviewer.css index 98110688e66..054159a1281 100644 --- a/src/vs/base/browser/ui/resourceviewer/resourceviewer.css +++ b/src/vs/base/browser/ui/resourceviewer/resourceviewer.css @@ -35,13 +35,14 @@ linear-gradient(45deg, rgb(20, 20, 20) 25%, transparent 25%, transparent 75%, rgb(20, 20, 20) 75%, rgb(20, 20, 20)); } +.monaco-resource-viewer img.pixelated { + image-rendering: pixelated; +} + .monaco-resource-viewer img.untouched { max-width: 100%; object-fit: contain; -} - -.monaco-resource-viewer img.pixelated { - image-rendering: pixelated; + image-rendering: auto; } .monaco-resource-viewer img { From eac87dab44e8f7a0cb841bc6ce514bf0ccdaf310 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Mon, 22 Jan 2018 16:41:21 -0800 Subject: [PATCH 083/128] Extract largeImageView Extract genericBinaryFileView Extract inlineImageView Add explicit return nulls for largeImageView and binaryFile View Extract getMime Use or return type in getMime Extract size logic to own class Move IMAGE_SCALE_CACHE into class Extract LargeImageView to own class Extract GenericBinaryFileView to class Move InlineImageView to own class Extract image view to own class Format param list Move imgSource into InlineImageView Make metadataClb non-optional since it is always pasesd in Extract isImageResource Give inlineImage a more descriptive name --- .../ui/resourceviewer/resourceViewer.ts | 450 +++++++++--------- 1 file changed, 237 insertions(+), 213 deletions(-) diff --git a/src/vs/base/browser/ui/resourceviewer/resourceViewer.ts b/src/vs/base/browser/ui/resourceviewer/resourceViewer.ts index e1901ab0748..86ac9fbeee4 100644 --- a/src/vs/base/browser/ui/resourceviewer/resourceViewer.ts +++ b/src/vs/base/browser/ui/resourceviewer/resourceViewer.ts @@ -83,35 +83,33 @@ enum ScaleDirection { IN, OUT, } -// Chrome is caching images very aggressively and so we use the ETag information to find out if -// we need to bypass the cache or not. We could always bypass the cache everytime we show the image -// however that has very bad impact on memory consumption because each time the image gets shown, -// memory grows (see also https://github.com/electron/electron/issues/6275) -const IMAGE_RESOURCE_ETAG_CACHE = new LRUCache(100); -function imageSrc(descriptor: IResourceDescriptor): string { - if (descriptor.resource.scheme === Schemas.data) { - return descriptor.resource.toString(true /* skip encoding */); +class BinarySize { + public static readonly KB = 1024; + public static readonly MB = BinarySize.KB * BinarySize.KB; + public static readonly GB = BinarySize.MB * BinarySize.KB; + public static readonly TB = BinarySize.GB * BinarySize.KB; + + public static formatSize(size: number): string { + if (size < BinarySize.KB) { + return nls.localize('sizeB', "{0}B", size); + } + + if (size < BinarySize.MB) { + return nls.localize('sizeKB', "{0}KB", (size / BinarySize.KB).toFixed(2)); + } + + if (size < BinarySize.GB) { + return nls.localize('sizeMB', "{0}MB", (size / BinarySize.MB).toFixed(2)); + } + + if (size < BinarySize.TB) { + return nls.localize('sizeGB', "{0}GB", (size / BinarySize.GB).toFixed(2)); + } + + return nls.localize('sizeTB', "{0}TB", (size / BinarySize.TB).toFixed(2)); } - - const src = descriptor.resource.toString(); - - let cached = IMAGE_RESOURCE_ETAG_CACHE.get(src); - if (!cached) { - cached = { etag: descriptor.etag, src }; - IMAGE_RESOURCE_ETAG_CACHE.set(src, cached); - } - - if (cached.etag !== descriptor.etag) { - cached.etag = descriptor.etag; - cached.src = `${src}?${Date.now()}`; // bypass cache with this trick - } - - return cached.src; } -// store the scale of an image so it can be restored when changing editor tabs -const IMAGE_SCALE_CACHE = new LRUCache(100); - export interface ResourceViewerContext { layout(dimension: Dimension); } @@ -121,32 +119,30 @@ export interface ResourceViewerContext { * progress of the binary resource. */ export class ResourceViewer { - - private static readonly KB = 1024; - private static readonly MB = ResourceViewer.KB * ResourceViewer.KB; - private static readonly GB = ResourceViewer.MB * ResourceViewer.KB; - private static readonly TB = ResourceViewer.GB * ResourceViewer.KB; - - private static readonly MAX_IMAGE_SIZE = ResourceViewer.MB; // showing images inline is memory intense, so we have a limit - - private static readonly SCALE_PINCH_FACTOR = 0.1; - private static readonly SCALE_FACTOR = 1.5; - private static readonly MAX_SCALE = 20; - private static readonly MIN_SCALE = 0.1; - private static readonly PIXELATION_THRESHOLD = 64; // enable image-rendering: pixelated for images less than this - public static show( descriptor: IResourceDescriptor, container: Builder, scrollbar: DomScrollableElement, openExternal: (uri: URI) => void, - metadataClb?: (meta: string) => void + metadataClb: (meta: string) => void ): ResourceViewerContext { - // Ensure CSS class $(container).setClass('monaco-resource-viewer'); - // Lookup media mime if any + if (ResourceViewer.isImageResource(descriptor)) { + return ImageView.create(container, descriptor, scrollbar, openExternal, metadataClb); + } + + GenericBinaryFileView.create(container, metadataClb, descriptor, scrollbar); + return null; + } + + private static isImageResource(descriptor: IResourceDescriptor) { + const mime = ResourceViewer.getMime(descriptor); + return mime.indexOf('image/') >= 0; + } + + private static getMime(descriptor: IResourceDescriptor): string { let mime = descriptor.mime; if (!mime && descriptor.resource.scheme === Schemas.file) { const ext = paths.extname(descriptor.resource.toString()); @@ -154,163 +150,29 @@ export class ResourceViewer { mime = mapExtToMediaMimes[ext.toLowerCase()]; } } + return mime || mimes.MIME_BINARY; + } +} - if (!mime) { - mime = mimes.MIME_BINARY; - } - - // Show Image inline unless they are large - if (mime.indexOf('image/') >= 0) { - if (ResourceViewer.inlineImage(descriptor)) { - const context = { - layout(dimension: Dimension) { } - }; - $(container) - .empty() - .addClass('image', 'zoom-in') - .img({ src: imageSrc(descriptor) }) - .addClass('untouched') - .on(DOM.EventType.LOAD, (e, img) => { - const imgElement = img.getHTMLElement(); - const cacheKey = descriptor.resource.toString(); - let scaleDirection = ScaleDirection.IN; - let scale = IMAGE_SCALE_CACHE.get(cacheKey) || null; - if (scale) { - img.removeClass('untouched'); - updateScale(scale); - } - - if (imgElement.naturalWidth < ResourceViewer.PIXELATION_THRESHOLD - || imgElement.naturalHeight < ResourceViewer.PIXELATION_THRESHOLD - ) { - img.addClass('pixelated'); - } - - function setImageWidth(width) { - img.style('width', `${width}px`); - img.style('height', 'auto'); - } - - function updateScale(newScale) { - scale = clamp(newScale, ResourceViewer.MIN_SCALE, ResourceViewer.MAX_SCALE); - setImageWidth(Math.floor(imgElement.naturalWidth * scale)); - IMAGE_SCALE_CACHE.set(cacheKey, scale); - - scrollbar.scanDomNode(); - - updateMetadata(); - } - - function updateMetadata() { - if (metadataClb) { - const scale = Math.round((imgElement.width / imgElement.naturalWidth) * 10000) / 100; - metadataClb(nls.localize('imgMeta', '{0}% {1}x{2} {3}', - scale, - imgElement.naturalWidth, - imgElement.naturalHeight, - ResourceViewer.formatSize(descriptor.size))); - } - } - - context.layout = updateMetadata; - - function firstZoom() { - const { clientWidth, naturalWidth } = imgElement; - setImageWidth(clientWidth); - img.removeClass('untouched'); - scale = clientWidth / naturalWidth; - } - - $(container) - .on(DOM.EventType.KEY_DOWN, (e: KeyboardEvent, c) => { - if (e.altKey) { - scaleDirection = ScaleDirection.OUT; - c.removeClass('zoom-in').addClass('zoom-out'); - } - }) - .on(DOM.EventType.KEY_UP, (e: KeyboardEvent, c) => { - if (!e.altKey) { - scaleDirection = ScaleDirection.IN; - c.removeClass('zoom-out').addClass('zoom-in'); - } - }); - - $(container).on(DOM.EventType.MOUSE_DOWN, (e: MouseEvent) => { - if (scale === null) { - firstZoom(); - } - - // right click - if (e.button === 2) { - updateScale(1); - } else { - const scaleFactor = scaleDirection === ScaleDirection.IN - ? ResourceViewer.SCALE_FACTOR - : 1 / ResourceViewer.SCALE_FACTOR; - - updateScale(scale * scaleFactor); - } - }); - - $(container).on(DOM.EventType.WHEEL, (e: WheelEvent) => { - // pinching is reported as scroll wheel + ctrl - if (!e.ctrlKey) { - return; - } - - if (scale === null) { - firstZoom(); - } - - // scrolling up, pinching out should increase the scale - const delta = -e.deltaY; - updateScale(scale + delta * ResourceViewer.SCALE_PINCH_FACTOR); - }); - - updateMetadata(); - - scrollbar.scanDomNode(); - }); - - return context; - } else { - const imageContainer = $(container) - .empty() - .p({ - text: nls.localize('largeImageError', "The image is too large to display in the editor. ") - }); - - if (descriptor.resource.scheme !== Schemas.data) { - imageContainer.append($('a', { - role: 'button', - class: 'open-external', - text: nls.localize('resourceOpenExternalButton', "Open image using external program?") - }).on(DOM.EventType.CLICK, (e) => { - openExternal(descriptor.resource); - })); - } - } - } - - // Handle generic Binary Files - else { - $(container) - .empty() - .span({ - text: nls.localize('nativeBinaryError', "The file will not be displayed in the editor because it is either binary, very large or uses an unsupported text encoding.") - }); - - if (metadataClb) { - metadataClb(ResourceViewer.formatSize(descriptor.size)); - } - - scrollbar.scanDomNode(); +class ImageView { + private static readonly MAX_IMAGE_SIZE = BinarySize.MB; // showing images inline is memory intense, so we have a limit + + public static create( + container: Builder, + descriptor: IResourceDescriptor, + scrollbar: DomScrollableElement, + openExternal: (uri: URI) => void, + metadataClb: (meta: string) => void + ): ResourceViewerContext | null { + if (ImageView.shouldShowImageInline(descriptor)) { + return InlineImageView.create(container, descriptor, scrollbar, metadataClb); } + LargeImageView.create(container, descriptor, openExternal); return null; } - private static inlineImage(descriptor: IResourceDescriptor): boolean { + private static shouldShowImageInline(descriptor: IResourceDescriptor): boolean { let skipInlineImage: boolean; // Data URI @@ -319,34 +181,196 @@ export class ResourceViewer { const base64MarkerIndex = descriptor.resource.path.indexOf(BASE64_MARKER); const hasData = base64MarkerIndex >= 0 && descriptor.resource.path.substring(base64MarkerIndex + BASE64_MARKER.length).length > 0; - skipInlineImage = !hasData || descriptor.size > ResourceViewer.MAX_IMAGE_SIZE || descriptor.resource.path.length > ResourceViewer.MAX_IMAGE_SIZE; + skipInlineImage = !hasData || descriptor.size > ImageView.MAX_IMAGE_SIZE || descriptor.resource.path.length > ImageView.MAX_IMAGE_SIZE; } // File URI else { - skipInlineImage = typeof descriptor.size !== 'number' || descriptor.size > ResourceViewer.MAX_IMAGE_SIZE; + skipInlineImage = typeof descriptor.size !== 'number' || descriptor.size > ImageView.MAX_IMAGE_SIZE; } return !skipInlineImage; } +} - private static formatSize(size: number): string { - if (size < ResourceViewer.KB) { - return nls.localize('sizeB', "{0}B", size); +class LargeImageView { + public static create( + container: Builder, + descriptor: IResourceDescriptor, + openExternal: (uri: URI) => void + ) { + const imageContainer = $(container) + .empty() + .p({ + text: nls.localize('largeImageError', "The image is too large to display in the editor. ") + }); + + if (descriptor.resource.scheme !== Schemas.data) { + imageContainer.append($('a', { + role: 'button', + class: 'open-external', + text: nls.localize('resourceOpenExternalButton', "Open image using external program?") + }).on(DOM.EventType.CLICK, (e) => { + openExternal(descriptor.resource); + })); } - - if (size < ResourceViewer.MB) { - return nls.localize('sizeKB', "{0}KB", (size / ResourceViewer.KB).toFixed(2)); - } - - if (size < ResourceViewer.GB) { - return nls.localize('sizeMB', "{0}MB", (size / ResourceViewer.MB).toFixed(2)); - } - - if (size < ResourceViewer.TB) { - return nls.localize('sizeGB', "{0}GB", (size / ResourceViewer.GB).toFixed(2)); - } - - return nls.localize('sizeTB', "{0}TB", (size / ResourceViewer.TB).toFixed(2)); + } +} + +class GenericBinaryFileView { + public static create( + container: Builder, + metadataClb: (meta: string) => void, + descriptor: IResourceDescriptor, + scrollbar: DomScrollableElement + ) { + $(container) + .empty() + .span({ + text: nls.localize('nativeBinaryError', "The file will not be displayed in the editor because it is either binary, very large or uses an unsupported text encoding.") + }); + if (metadataClb) { + metadataClb(BinarySize.formatSize(descriptor.size)); + } + scrollbar.scanDomNode(); + } +} + +class InlineImageView { + private static readonly SCALE_PINCH_FACTOR = 0.1; + private static readonly SCALE_FACTOR = 1.5; + private static readonly MAX_SCALE = 20; + private static readonly MIN_SCALE = 0.1; + private static readonly PIXELATION_THRESHOLD = 64; // enable image-rendering: pixelated for images less than this + + /** + * Chrome is caching images very aggressively and so we use the ETag information to find out if + * we need to bypass the cache or not. We could always bypass the cache everytime we show the image + * however that has very bad impact on memory consumption because each time the image gets shown, + * memory grows (see also https://github.com/electron/electron/issues/6275) + */ + private static IMAGE_RESOURCE_ETAG_CACHE = new LRUCache(100); + + /** + * Store the scale of an image so it can be restored when changing editor tabs + */ + private static readonly IMAGE_SCALE_CACHE = new LRUCache(100); + + public static create( + container: Builder, + descriptor: IResourceDescriptor, + scrollbar: DomScrollableElement, + metadataClb: (meta: string) => void + ) { + const context = { + layout(dimension: Dimension) { } + }; + $(container) + .empty() + .addClass('image', 'zoom-in') + .img({ src: InlineImageView.imageSrc(descriptor) }) + .addClass('untouched') + .on(DOM.EventType.LOAD, (e, img) => { + const imgElement = img.getHTMLElement(); + const cacheKey = descriptor.resource.toString(); + let scaleDirection = ScaleDirection.IN; + let scale = InlineImageView.IMAGE_SCALE_CACHE.get(cacheKey) || null; + if (scale) { + img.removeClass('untouched'); + updateScale(scale); + } + if (imgElement.naturalWidth < InlineImageView.PIXELATION_THRESHOLD + || imgElement.naturalHeight < InlineImageView.PIXELATION_THRESHOLD) { + img.addClass('pixelated'); + } + function setImageWidth(width) { + img.style('width', `${width}px`); + img.style('height', 'auto'); + } + function updateScale(newScale) { + scale = clamp(newScale, InlineImageView.MIN_SCALE, InlineImageView.MAX_SCALE); + setImageWidth(Math.floor(imgElement.naturalWidth * scale)); + InlineImageView.IMAGE_SCALE_CACHE.set(cacheKey, scale); + scrollbar.scanDomNode(); + updateMetadata(); + } + function updateMetadata() { + if (metadataClb) { + const scale = Math.round((imgElement.width / imgElement.naturalWidth) * 10000) / 100; + metadataClb(nls.localize('imgMeta', '{0}% {1}x{2} {3}', scale, imgElement.naturalWidth, imgElement.naturalHeight, BinarySize.formatSize(descriptor.size))); + } + } + context.layout = updateMetadata; + function firstZoom() { + const { clientWidth, naturalWidth } = imgElement; + setImageWidth(clientWidth); + img.removeClass('untouched'); + scale = clientWidth / naturalWidth; + } + $(container) + .on(DOM.EventType.KEY_DOWN, (e: KeyboardEvent, c) => { + if (e.altKey) { + scaleDirection = ScaleDirection.OUT; + c.removeClass('zoom-in').addClass('zoom-out'); + } + }) + .on(DOM.EventType.KEY_UP, (e: KeyboardEvent, c) => { + if (!e.altKey) { + scaleDirection = ScaleDirection.IN; + c.removeClass('zoom-out').addClass('zoom-in'); + } + }); + $(container).on(DOM.EventType.MOUSE_DOWN, (e: MouseEvent) => { + if (scale === null) { + firstZoom(); + } + // right click + if (e.button === 2) { + updateScale(1); + } + else { + const scaleFactor = scaleDirection === ScaleDirection.IN + ? InlineImageView.SCALE_FACTOR + : 1 / InlineImageView.SCALE_FACTOR; + updateScale(scale * scaleFactor); + } + }); + $(container).on(DOM.EventType.WHEEL, (e: WheelEvent) => { + // pinching is reported as scroll wheel + ctrl + if (!e.ctrlKey) { + return; + } + if (scale === null) { + firstZoom(); + } + // scrolling up, pinching out should increase the scale + const delta = -e.deltaY; + updateScale(scale + delta * InlineImageView.SCALE_PINCH_FACTOR); + }); + updateMetadata(); + scrollbar.scanDomNode(); + }); + return context; + } + + private static imageSrc(descriptor: IResourceDescriptor): string { + if (descriptor.resource.scheme === Schemas.data) { + return descriptor.resource.toString(true /* skip encoding */); + } + + const src = descriptor.resource.toString(); + + let cached = InlineImageView.IMAGE_RESOURCE_ETAG_CACHE.get(src); + if (!cached) { + cached = { etag: descriptor.etag, src }; + InlineImageView.IMAGE_RESOURCE_ETAG_CACHE.set(src, cached); + } + + if (cached.etag !== descriptor.etag) { + cached.etag = descriptor.etag; + cached.src = `${src}?${Date.now()}`; // bypass cache with this trick + } + + return cached.src; } } From f73e7bddfa40cacb09e390423eb4cb42687d2cd0 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Mon, 22 Jan 2018 17:24:49 -0800 Subject: [PATCH 084/128] Dont load loguploader using import expression --- src/vs/code/electron-main/main.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/code/electron-main/main.ts b/src/vs/code/electron-main/main.ts index ea7bf733406..8ba25df359b 100644 --- a/src/vs/code/electron-main/main.ts +++ b/src/vs/code/electron-main/main.ts @@ -45,6 +45,7 @@ import { mnemonicButtonLabel } from 'vs/base/common/labels'; import { createSpdLogService } from 'vs/platform/log/node/spdlogService'; import { printDiagnostics } from 'vs/code/electron-main/diagnostics'; import { BufferLogService } from 'vs/platform/log/common/bufferLog'; +import { uploadLogs } from 'vs/code/electron-main/logUploader'; function createServices(args: ParsedArgs, bufferLogService: BufferLogService): IInstantiationService { const services = new ServiceCollection(); @@ -198,8 +199,7 @@ function setupIPC(accessor: ServicesAccessor): TPromise { // Log uploader if (environmentService.args['upload-logs']) { - return import('vs/code/electron-main/logUploader') - .then(logUploader => logUploader.uploadLogs(channel, requestService)) + return uploadLogs(channel, requestService) .then(() => TPromise.wrapError(new ExpectedError())); } From 3877f2a950d2e0b096795b2bbf09af64edb387bb Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Mon, 22 Jan 2018 17:46:04 -0800 Subject: [PATCH 085/128] Fix parsing of result message in uploadlogs --- src/vs/code/electron-main/logUploader.ts | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/vs/code/electron-main/logUploader.ts b/src/vs/code/electron-main/logUploader.ts index f4a034627d3..795321e8600 100644 --- a/src/vs/code/electron-main/logUploader.ts +++ b/src/vs/code/electron-main/logUploader.ts @@ -94,12 +94,22 @@ async function postLogs( throw e; } - try { - return JSON.parse(result.stream.toString()); - } catch (e) { - console.log(localize('parseError', 'Error parsing response')); - throw e; - } + return new TPromise((res, reject) => { + const parts: Buffer[] = []; + result.stream.on('data', data => { + parts.push(data); + }); + + result.stream.on('end', () => { + try { + const result = Buffer.concat(parts).toString('utf-8'); + res(JSON.parse(result)); + } catch (e) { + console.log(localize('parseError', 'Error parsing response')); + reject(e); + } + }); + }); } function zipLogs( From 97904cdd61fbaef3321dd9c1b89328c7afdce201 Mon Sep 17 00:00:00 2001 From: Ramya Achutha Rao Date: Mon, 22 Jan 2018 17:47:21 -0800 Subject: [PATCH 086/128] Dont hide details when frozen Fixes #39742 --- src/vs/editor/contrib/suggest/suggestWidget.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/editor/contrib/suggest/suggestWidget.ts b/src/vs/editor/contrib/suggest/suggestWidget.ts index 6624f1ca547..05c83cead70 100644 --- a/src/vs/editor/contrib/suggest/suggestWidget.ts +++ b/src/vs/editor/contrib/suggest/suggestWidget.ts @@ -667,7 +667,7 @@ export class SuggestWidget implements IContentWidget, IDelegate this.show(); break; case State.Frozen: - hide(this.messageElement, this.details.element); + hide(this.messageElement); show(this.listElement); this.show(); break; From 65064fd524f4e94605126adba9c15506eb1829b5 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Mon, 22 Jan 2018 18:30:59 -0800 Subject: [PATCH 087/128] Make sure log body is properly encoded --- src/vs/code/electron-main/logUploader.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/vs/code/electron-main/logUploader.ts b/src/vs/code/electron-main/logUploader.ts index 795321e8600..bbcc8a6a76a 100644 --- a/src/vs/code/electron-main/logUploader.ts +++ b/src/vs/code/electron-main/logUploader.ts @@ -83,10 +83,9 @@ async function postLogs( result = await requestService.request({ url: endpoint.url, type: 'POST', - data: fs.createReadStream(outZip), + data: new Buffer(fs.readFileSync(outZip)).toString('base64'), headers: { - 'Content-Type': 'application/zip', - 'Content-Length': fs.statSync(outZip).size + 'Content-Type': 'application/zip' } }); } catch (e) { From 00f0f2c8962bfad8201d225cdb46646fb5f104f0 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Mon, 22 Jan 2018 18:36:37 -0800 Subject: [PATCH 088/128] Use special prefix to tell TS that a resource is in-memory only (#42001) * Use special prefix to tell TS that a resource is in memory * Move scheme checking logic into getWorkspaceRootForResource --- .../src/features/bufferSyncSupport.ts | 6 +-- .../typescript/src/typescriptServiceClient.ts | 37 +++++++++++++------ 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/extensions/typescript/src/features/bufferSyncSupport.ts b/extensions/typescript/src/features/bufferSyncSupport.ts index 63f0bdfbc86..f3b005cdb4b 100644 --- a/extensions/typescript/src/features/bufferSyncSupport.ts +++ b/extensions/typescript/src/features/bufferSyncSupport.ts @@ -10,6 +10,7 @@ import * as Proto from '../protocol'; import { ITypeScriptServiceClient } from '../typescriptService'; import { Delayer } from '../utils/async'; import * as languageModeIds from '../utils/languageModeIds'; +import * as fileSchemes from '../utils/fileSchemes'; interface IDiagnosticRequestor { requestDiagnostic(filepath: string): void; @@ -48,10 +49,7 @@ class SyncedBuffer { } if (this.client.apiVersion.has230Features()) { - const root = this.client.getWorkspaceRootForResource(this.document.uri); - if (root) { - args.projectRootPath = root; - } + args.projectRootPath = this.client.getWorkspaceRootForResource(this.document.uri); } if (this.client.apiVersion.has240Features()) { diff --git a/extensions/typescript/src/typescriptServiceClient.ts b/extensions/typescript/src/typescriptServiceClient.ts index 2901d184ba1..a36818b3905 100644 --- a/extensions/typescript/src/typescriptServiceClient.ts +++ b/extensions/typescript/src/typescriptServiceClient.ts @@ -578,12 +578,12 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient } public normalizePath(resource: Uri): string | null { - if (resource.scheme === fileSchemes.walkThroughSnippet) { - return resource.toString(); - } - - if (resource.scheme === fileSchemes.untitled && this._apiVersion.has213Features()) { - return resource.toString(); + if (this._apiVersion.has213Features()) { + if (resource.scheme === fileSchemes.walkThroughSnippet || resource.scheme === fileSchemes.untitled) { + const dirName = path.dirname(resource.path); + const fileName = this.inMemoryResourcePrefix + path.basename(resource.path); + return resource.with({ path: path.join(dirName, fileName) }).toString(true); + } } if (resource.scheme !== fileSchemes.file) { @@ -599,11 +599,24 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient return result.replace(new RegExp('\\' + this.pathSeparator, 'g'), '/'); } + private get inMemoryResourcePrefix(): string { + return this._apiVersion.has270Features() ? '^' : ''; + } + public asUrl(filepath: string): Uri { - if (filepath.startsWith(TypeScriptServiceClient.WALK_THROUGH_SNIPPET_SCHEME_COLON) - || (filepath.startsWith(fileSchemes.untitled + ':') && this._apiVersion.has213Features()) - ) { - return Uri.parse(filepath); + if (this._apiVersion.has213Features()) { + if (filepath.startsWith(TypeScriptServiceClient.WALK_THROUGH_SNIPPET_SCHEME_COLON) || (filepath.startsWith(fileSchemes.untitled + ':')) + ) { + let resource = Uri.parse(filepath); + if (this.inMemoryResourcePrefix) { + const dirName = path.dirname(resource.path); + const fileName = path.basename(resource.path); + if (fileName.startsWith(this.inMemoryResourcePrefix)) { + resource = resource.with({ path: path.join(dirName, fileName.slice(this.inMemoryResourcePrefix.length)) }); + } + } + return resource; + } } return Uri.file(filepath); } @@ -620,8 +633,10 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient return root.uri.fsPath; } } + return roots[0].uri.fsPath; } - return roots[0].uri.fsPath; + + return undefined; } public execute(command: string, args: any, expectsResultOrToken?: boolean | CancellationToken): Promise { From 75b3282ec2215c7af5eda9855c94e42d42e666ac Mon Sep 17 00:00:00 2001 From: Peng Lyu Date: Mon, 22 Jan 2018 17:30:00 -0800 Subject: [PATCH 089/128] Test cases for pieceTree.equal --- .../pieceTreeTextBuffer.test.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/vs/editor/test/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.test.ts b/src/vs/editor/test/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.test.ts index 83eaf75bde0..fad324b8051 100644 --- a/src/vs/editor/test/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.test.ts +++ b/src/vs/editor/test/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.test.ts @@ -1517,4 +1517,18 @@ suite('buffer api', () => { assert(!a.equal(c)); assert(!a.equal(d)); }); + + test('equal 2, empty buffer', () => { + let a = createTextBuffer(['']); + let b = createTextBuffer(['']); + + assert(a.equal(b)); + }); + + test('equal 3, empty buffer', () => { + let a = createTextBuffer(['a']); + let b = createTextBuffer(['']); + + assert(!a.equal(b)); + }); }); \ No newline at end of file From fa455a7249393df1d8cc1071f23ae91599102336 Mon Sep 17 00:00:00 2001 From: Peng Lyu Date: Mon, 22 Jan 2018 18:54:36 -0800 Subject: [PATCH 090/128] Revert "First attempt of search caching." This reverts commit 0f907abc022460b9b9f114ea65ce181a4239d54f. --- .../pieceTreeTextBuffer/pieceTreeBase.ts | 485 ++++++++++++++++-- .../model/pieceTreeTextBuffer/rbTreeBase.ts | 427 --------------- .../pieceTreeTextBuffer.test.ts | 3 +- 3 files changed, 441 insertions(+), 474 deletions(-) delete mode 100644 src/vs/editor/common/model/pieceTreeTextBuffer/rbTreeBase.ts diff --git a/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts b/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts index e9e428b9106..9598964eff9 100644 --- a/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts +++ b/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts @@ -8,7 +8,49 @@ import { Position } from 'vs/editor/common/core/position'; import { CharCode } from 'vs/base/common/charCode'; import { Range } from 'vs/editor/common/core/range'; import { ITextSnapshot } from 'vs/platform/files/common/files'; -import { leftest, righttest, updateTreeMetadata, rbDelete, fixInsert, NodeColor, SENTINEL, TreeNode } from 'vs/editor/common/model/pieceTreeTextBuffer/rbTreeBase'; + +export const enum NodeColor { + Black = 0, + Red = 1, +} + +export function getNodeColor(node: TreeNode) { + return node.color; +} + +function leftest(node: TreeNode): TreeNode { + while (node.left !== SENTINEL) { + node = node.left; + } + return node; +} + +function righttest(node: TreeNode): TreeNode { + while (node.right !== SENTINEL) { + node = node.right; + } + return node; +} + +function calculateSize(node: TreeNode): number { + if (node === SENTINEL) { + return 0; + } + + return node.size_left + node.piece.length + calculateSize(node.right); +} + +function calculateLF(node: TreeNode): number { + if (node === SENTINEL) { + return 0; + } + + return node.lf_left + node.piece.lineFeedCnt + calculateLF(node.right); +} + +function resetSentinel(): void { + SENTINEL.parent = SENTINEL; +} // const lfRegex = new RegExp(/\r\n|\r|\n/g); @@ -96,6 +138,84 @@ export function createLineStarts(r: number[], str: string): LineStarts { return result; } +export class TreeNode { + parent: TreeNode; + left: TreeNode; + right: TreeNode; + color: NodeColor; + + // Piece + piece: Piece; + size_left: number; // size of the left subtree (not inorder) + lf_left: number; // line feeds cnt in the left subtree (not in order) + + constructor(piece: Piece, color: NodeColor) { + this.piece = piece; + this.color = color; + this.size_left = 0; + this.lf_left = 0; + this.parent = null; + this.left = null; + this.right = null; + } + + public next(): TreeNode { + if (this.right !== SENTINEL) { + return leftest(this.right); + } + + let node: TreeNode = this; + + while (node.parent !== SENTINEL) { + if (node.parent.left === node) { + break; + } + + node = node.parent; + } + + if (node.parent === SENTINEL) { + return SENTINEL; + } else { + return node.parent; + } + } + + public prev(): TreeNode { + if (this.left !== SENTINEL) { + return righttest(this.left); + } + + let node: TreeNode = this; + + while (node.parent !== SENTINEL) { + if (node.parent.right === node) { + break; + } + + node = node.parent; + } + + if (node.parent === SENTINEL) { + return SENTINEL; + } else { + return node.parent; + } + } + + public detach(): void { + this.parent = null; + this.left = null; + this.right = null; + } +} + +export const SENTINEL: TreeNode = new TreeNode(null, NodeColor.Black); +SENTINEL.parent = SENTINEL; +SENTINEL.left = SENTINEL; +SENTINEL.right = SENTINEL; +SENTINEL.color = NodeColor.Black; + export interface NodePosition { /** * Piece Index @@ -202,7 +322,6 @@ export class PieceTreeBase { protected _lineCnt: number; protected _length: number; private _lastChangeBufferPos: BufferCursor; - private _lastNodePosition: NodePosition; constructor(chunks: StringBuffer[]) { this.create(chunks); @@ -213,7 +332,6 @@ export class PieceTreeBase { new StringBuffer('', [0]) ]; this._lastChangeBufferPos = { line: 0, column: 0 }; - this._lastNodePosition = null; this.root = SENTINEL; this._lineCnt = 1; this._length = 0; @@ -275,6 +393,7 @@ export class PieceTreeBase { this.create(chunks); } + // #region Buffer API public createSnapshot(BOM: string): ITextSnapshot { return new PieceTreeSnapshot(this, BOM); @@ -453,7 +572,6 @@ export class PieceTreeBase { // changed buffer this.appendToNode(node, value); this.computeBufferMetadata(); - this._lastNodePosition = { node, remainder, nodeStartOffset }; return; } @@ -536,7 +654,7 @@ export class PieceTreeBase { if (startPosition.nodeStartOffset === offset) { if (cnt === startNode.piece.length) { // delete node let next = startNode.next(); - rbDelete(this, startNode); + this.rbDelete(startNode); this.validateCRLFWithPrevNode(next); this.computeBufferMetadata(); return; @@ -600,7 +718,7 @@ export class PieceTreeBase { piece.length -= 1; value += '\n'; - updateTreeMetadata(this, node, -1, -1); + this.updateTreeMetadata(node, -1, -1); if (node.piece.length === 0) { nodesToDel.push(node); @@ -704,7 +822,7 @@ export class PieceTreeBase { deleteNodes(nodes: TreeNode[]): void { for (let i = 0; i < nodes.length; i++) { - rbDelete(this, nodes[i]); + this.rbDelete(nodes[i]); } } @@ -860,7 +978,7 @@ export class PieceTreeBase { let lf_delta = piece.lineFeedCnt - originalLFCnt; let size_delta = newEndOffset - originalEndOffset; piece.length += size_delta; - updateTreeMetadata(this, node, size_delta, lf_delta); + this.updateTreeMetadata(node, size_delta, lf_delta); } deleteNodeHead(node: TreeNode, pos: BufferCursor) { @@ -874,7 +992,7 @@ export class PieceTreeBase { let lf_delta = piece.lineFeedCnt - originalLFCnt; let size_delta = originalStartOffset - newStartOffset; piece.length += size_delta; - updateTreeMetadata(this, node, size_delta, lf_delta); + this.updateTreeMetadata(node, size_delta, lf_delta); } shrinkNode(node: TreeNode, start: BufferCursor, end: BufferCursor) { @@ -890,7 +1008,7 @@ export class PieceTreeBase { let newLength = this.offsetInBuffer(piece.bufferIndex, start) - this.offsetInBuffer(piece.bufferIndex, originalStartPos); let newLFCnt = piece.lineFeedCnt; piece.length = newLength; - updateTreeMetadata(this, node, newLength - oldLength, newLFCnt - oldLFCnt); + this.updateTreeMetadata(node, newLength - oldLength, newLFCnt - oldLFCnt); // new right piece, end, originalEndPos let newPiece = new Piece( @@ -934,36 +1052,10 @@ export class PieceTreeBase { node.piece.lineFeedCnt = newLineFeedCnt; let lf_delta = newLineFeedCnt - oldLineFeedCnt; this._lastChangeBufferPos = endPos; - updateTreeMetadata(this, node, value.length, lf_delta); - } - - readNodePositionFromCache(offset: number): NodePosition { - if (!this._lastNodePosition) { - return null; - } - - if (this._lastNodePosition.node.parent === null) { - this._lastNodePosition = null; - return null; - } - - if (this._lastNodePosition.nodeStartOffset > offset || this._lastNodePosition.nodeStartOffset + this._lastNodePosition.node.piece.length < offset) { - return null; - } - - return { - node: this._lastNodePosition.node, - remainder: offset - this._lastNodePosition.nodeStartOffset, - nodeStartOffset: this._lastNodePosition.nodeStartOffset - }; + this.updateTreeMetadata(node, value.length, lf_delta); } nodeAt(offset: number): NodePosition { - let cachedNodePosition = this.readNodePositionFromCache(offset); - if (cachedNodePosition) { - return cachedNodePosition; - } - let x = this.root; let nodeStartOffset = 0; @@ -1155,7 +1247,7 @@ export class PieceTreeBase { prev.piece.length -= 1; prev.piece.lineFeedCnt -= 1; - updateTreeMetadata(this, prev, - 1, -1); + this.updateTreeMetadata(prev, - 1, -1); if (prev.piece.length === 0) { nodesToDel.push(prev); } @@ -1167,7 +1259,7 @@ export class PieceTreeBase { next.piece.lineFeedCnt = this.getLineFeedCnt(next.piece.bufferIndex, next.piece.start, next.piece.end); // @todo, we can optimize // } - updateTreeMetadata(this, next, - 1, -1); + this.updateTreeMetadata(next, - 1, -1); if (next.piece.length === 0) { nodesToDel.push(next); } @@ -1178,7 +1270,7 @@ export class PieceTreeBase { // delete empty nodes for (let i = 0; i < nodesToDel.length; i++) { - rbDelete(this, nodesToDel[i]); + this.rbDelete(nodesToDel[i]); } } @@ -1190,7 +1282,7 @@ export class PieceTreeBase { value += '\n'; if (nextNode.piece.length === 1) { - rbDelete(this, nextNode); + this.rbDelete(nextNode); } else { let piece = nextNode.piece; @@ -1198,7 +1290,7 @@ export class PieceTreeBase { piece.start = newStart; piece.length -= 1; piece.lineFeedCnt = this.getLineFeedCnt(piece.bufferIndex, piece.start, piece.end); // @todo, we can optimize - updateTreeMetadata(this, nextNode, -1, -1); + this.updateTreeMetadata(nextNode, -1, -1); } return true; } @@ -1211,7 +1303,7 @@ export class PieceTreeBase { // #endregion - // #region Tree operations + // #region Red Black Tree iterate(node: TreeNode, callback: (node: TreeNode) => boolean): boolean { if (node === SENTINEL) { return callback(SENTINEL); @@ -1238,6 +1330,53 @@ export class PieceTreeBase { return currentContent; } + leftRotate(x: TreeNode) { + let y = x.right; + + // fix size_left + y.size_left += x.size_left + (x.piece ? x.piece.length : 0); + y.lf_left += x.lf_left + (x.piece ? x.piece.lineFeedCnt : 0); + x.right = y.left; + + if (y.left !== SENTINEL) { + y.left.parent = x; + } + y.parent = x.parent; + if (x.parent === SENTINEL) { + this.root = y; + } else if (x.parent.left === x) { + x.parent.left = y; + } else { + x.parent.right = y; + } + y.left = x; + x.parent = y; + } + + rightRotate(y: TreeNode) { + let x = y.left; + y.left = x.right; + if (x.right !== SENTINEL) { + x.right.parent = y; + } + x.parent = y.parent; + + // fix size_left + y.size_left -= x.size_left + (x.piece ? x.piece.length : 0); + y.lf_left -= x.lf_left + (x.piece ? x.piece.lineFeedCnt : 0); + + if (y.parent === SENTINEL) { + this.root = x; + } else if (y === y.parent.right) { + y.parent.right = x; + } else { + y.parent.left = x; + } + + x.right = y; + y.parent = x; + } + /** * node node * / \ / \ @@ -1266,7 +1405,7 @@ export class PieceTreeBase { z.parent = nextNode; } - fixInsert(this, z); + this.fixInsert(z); return z; } @@ -1298,10 +1437,266 @@ export class PieceTreeBase { z.parent = prevNode; } - fixInsert(this, z); + this.fixInsert(z); return z; } + rbDelete(z: TreeNode) { + let x: TreeNode; + let y: TreeNode; + + if (z.left === SENTINEL) { + y = z; + x = y.right; + } else if (z.right === SENTINEL) { + y = z; + x = y.left; + } else { + y = leftest(z.right); + x = y.right; + } + + if (y === this.root) { + this.root = x; + + // if x is null, we are removing the only node + x.color = NodeColor.Black; + z.detach(); + resetSentinel(); + this.root.parent = SENTINEL; + + return; + } + + let yWasRed = (y.color === NodeColor.Red); + + if (y === y.parent.left) { + y.parent.left = x; + } else { + y.parent.right = x; + } + + if (y === z) { + x.parent = y.parent; + this.recomputeTreeMetadata(x); + } else { + if (y.parent === z) { + x.parent = y; + } else { + x.parent = y.parent; + } + + // as we make changes to x's hierarchy, update size_left of subtree first + this.recomputeTreeMetadata(x); + + y.left = z.left; + y.right = z.right; + y.parent = z.parent; + y.color = z.color; + + if (z === this.root) { + this.root = y; + } else { + if (z === z.parent.left) { + z.parent.left = y; + } else { + z.parent.right = y; + } + } + + if (y.left !== SENTINEL) { + y.left.parent = y; + } + if (y.right !== SENTINEL) { + y.right.parent = y; + } + // update metadata + // we replace z with y, so in this sub tree, the length change is z.item.length + y.size_left = z.size_left; + y.lf_left = z.lf_left; + this.recomputeTreeMetadata(y); + } + + z.detach(); + + if (x.parent.left === x) { + let newSizeLeft = calculateSize(x); + let newLFLeft = calculateLF(x); + if (newSizeLeft !== x.parent.size_left || newLFLeft !== x.parent.lf_left) { + let delta = newSizeLeft - x.parent.size_left; + let lf_delta = newLFLeft - x.parent.lf_left; + x.parent.size_left = newSizeLeft; + x.parent.lf_left = newLFLeft; + this.updateTreeMetadata(x.parent, delta, lf_delta); + } + } + + this.recomputeTreeMetadata(x.parent); + + if (yWasRed) { + resetSentinel(); + return; + } + + // RB-DELETE-FIXUP + let w: TreeNode; + while (x !== this.root && x.color === NodeColor.Black) { + if (x === x.parent.left) { + w = x.parent.right; + + if (w.color === NodeColor.Red) { + w.color = NodeColor.Black; + x.parent.color = NodeColor.Red; + this.leftRotate(x.parent); + w = x.parent.right; + } + + if (w.left.color === NodeColor.Black && w.right.color === NodeColor.Black) { + w.color = NodeColor.Red; + x = x.parent; + } else { + if (w.right.color === NodeColor.Black) { + w.left.color = NodeColor.Black; + w.color = NodeColor.Red; + this.rightRotate(w); + w = x.parent.right; + } + + w.color = x.parent.color; + x.parent.color = NodeColor.Black; + w.right.color = NodeColor.Black; + this.leftRotate(x.parent); + x = this.root; + } + } else { + w = x.parent.left; + + if (w.color === NodeColor.Red) { + w.color = NodeColor.Black; + x.parent.color = NodeColor.Red; + this.rightRotate(x.parent); + w = x.parent.left; + } + + if (w.left.color === NodeColor.Black && w.right.color === NodeColor.Black) { + w.color = NodeColor.Red; + x = x.parent; + + } else { + if (w.left.color === NodeColor.Black) { + w.right.color = NodeColor.Black; + w.color = NodeColor.Red; + this.leftRotate(w); + w = x.parent.left; + } + + w.color = x.parent.color; + x.parent.color = NodeColor.Black; + w.left.color = NodeColor.Black; + this.rightRotate(x.parent); + x = this.root; + } + } + } + x.color = NodeColor.Black; + resetSentinel(); + } + + fixInsert(x: TreeNode) { + this.recomputeTreeMetadata(x); + + while (x !== this.root && x.parent.color === NodeColor.Red) { + if (x.parent === x.parent.parent.left) { + const y = x.parent.parent.right; + + if (y.color === NodeColor.Red) { + x.parent.color = NodeColor.Black; + y.color = NodeColor.Black; + x.parent.parent.color = NodeColor.Red; + x = x.parent.parent; + } else { + if (x === x.parent.right) { + x = x.parent; + this.leftRotate(x); + } + + x.parent.color = NodeColor.Black; + x.parent.parent.color = NodeColor.Red; + this.rightRotate(x.parent.parent); + } + } else { + const y = x.parent.parent.left; + + if (y.color === NodeColor.Red) { + x.parent.color = NodeColor.Black; + y.color = NodeColor.Black; + x.parent.parent.color = NodeColor.Red; + x = x.parent.parent; + } else { + if (x === x.parent.left) { + x = x.parent; + this.rightRotate(x); + } + x.parent.color = NodeColor.Black; + x.parent.parent.color = NodeColor.Red; + this.leftRotate(x.parent.parent); + } + } + } + + this.root.color = NodeColor.Black; + } + + updateTreeMetadata(x: TreeNode, delta: number, lineFeedCntDelta: number): void { + // node length change or line feed count change + while (x !== this.root && x !== SENTINEL) { + if (x.parent.left === x) { + x.parent.size_left += delta; + x.parent.lf_left += lineFeedCntDelta; + } + + x = x.parent; + } + } + + recomputeTreeMetadata(x: TreeNode) { + let delta = 0; + let lf_delta = 0; + if (x === this.root) { + return; + } + + if (delta === 0) { + // go upwards till the node whose left subtree is changed. + while (x !== this.root && x === x.parent.right) { + x = x.parent; + } + + if (x === this.root) { + // well, it means we add a node to the end (inorder) + return; + } + + // x is the node whose right subtree is changed. + x = x.parent; + + delta = calculateSize(x.left) - x.size_left; + lf_delta = calculateLF(x.left) - x.lf_left; + x.size_left += delta; + x.lf_left += lf_delta; + } + + // go upwards till root. O(logN) + while (x !== this.root && (delta !== 0 || lf_delta !== 0)) { + if (x.parent.left === x) { + x.parent.size_left += delta; + x.parent.lf_left += lf_delta; + } + + x = x.parent; + } + } + getContentOfSubTree(node: TreeNode): string { let str = ''; diff --git a/src/vs/editor/common/model/pieceTreeTextBuffer/rbTreeBase.ts b/src/vs/editor/common/model/pieceTreeTextBuffer/rbTreeBase.ts deleted file mode 100644 index 84417b947cd..00000000000 --- a/src/vs/editor/common/model/pieceTreeTextBuffer/rbTreeBase.ts +++ /dev/null @@ -1,427 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -'use strict'; - -import { Piece, PieceTreeBase } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase'; - -export class TreeNode { - parent: TreeNode; - left: TreeNode; - right: TreeNode; - color: NodeColor; - - // Piece - piece: Piece; - size_left: number; // size of the left subtree (not inorder) - lf_left: number; // line feeds cnt in the left subtree (not in order) - - constructor(piece: Piece, color: NodeColor) { - this.piece = piece; - this.color = color; - this.size_left = 0; - this.lf_left = 0; - this.parent = null; - this.left = null; - this.right = null; - } - - public next(): TreeNode { - if (this.right !== SENTINEL) { - return leftest(this.right); - } - - let node: TreeNode = this; - - while (node.parent !== SENTINEL) { - if (node.parent.left === node) { - break; - } - - node = node.parent; - } - - if (node.parent === SENTINEL) { - return SENTINEL; - } else { - return node.parent; - } - } - - public prev(): TreeNode { - if (this.left !== SENTINEL) { - return righttest(this.left); - } - - let node: TreeNode = this; - - while (node.parent !== SENTINEL) { - if (node.parent.right === node) { - break; - } - - node = node.parent; - } - - if (node.parent === SENTINEL) { - return SENTINEL; - } else { - return node.parent; - } - } - - public detach(): void { - this.parent = null; - this.left = null; - this.right = null; - } -} - -export const SENTINEL: TreeNode = new TreeNode(null, NodeColor.Black); -SENTINEL.parent = SENTINEL; -SENTINEL.left = SENTINEL; -SENTINEL.right = SENTINEL; -SENTINEL.color = NodeColor.Black; - -export const enum NodeColor { - Black = 0, - Red = 1, -} - -export function leftest(node: TreeNode): TreeNode { - while (node.left !== SENTINEL) { - node = node.left; - } - return node; -} - -export function righttest(node: TreeNode): TreeNode { - while (node.right !== SENTINEL) { - node = node.right; - } - return node; -} - -export function calculateSize(node: TreeNode): number { - if (node === SENTINEL) { - return 0; - } - - return node.size_left + node.piece.length + calculateSize(node.right); -} - -export function calculateLF(node: TreeNode): number { - if (node === SENTINEL) { - return 0; - } - - return node.lf_left + node.piece.lineFeedCnt + calculateLF(node.right); -} - -export function resetSentinel(): void { - SENTINEL.parent = SENTINEL; -} - -export function leftRotate(tree: PieceTreeBase, x: TreeNode) { - let y = x.right; - - // fix size_left - y.size_left += x.size_left + (x.piece ? x.piece.length : 0); - y.lf_left += x.lf_left + (x.piece ? x.piece.lineFeedCnt : 0); - x.right = y.left; - - if (y.left !== SENTINEL) { - y.left.parent = x; - } - y.parent = x.parent; - if (x.parent === SENTINEL) { - tree.root = y; - } else if (x.parent.left === x) { - x.parent.left = y; - } else { - x.parent.right = y; - } - y.left = x; - x.parent = y; -} - -export function rightRotate(tree: PieceTreeBase, y: TreeNode) { - let x = y.left; - y.left = x.right; - if (x.right !== SENTINEL) { - x.right.parent = y; - } - x.parent = y.parent; - - // fix size_left - y.size_left -= x.size_left + (x.piece ? x.piece.length : 0); - y.lf_left -= x.lf_left + (x.piece ? x.piece.lineFeedCnt : 0); - - if (y.parent === SENTINEL) { - tree.root = x; - } else if (y === y.parent.right) { - y.parent.right = x; - } else { - y.parent.left = x; - } - - x.right = y; - y.parent = x; -} - -export function rbDelete(tree: PieceTreeBase, z: TreeNode) { - let x: TreeNode; - let y: TreeNode; - - if (z.left === SENTINEL) { - y = z; - x = y.right; - } else if (z.right === SENTINEL) { - y = z; - x = y.left; - } else { - y = leftest(z.right); - x = y.right; - } - - if (y === tree.root) { - tree.root = x; - - // if x is null, we are removing the only node - x.color = NodeColor.Black; - z.detach(); - resetSentinel(); - tree.root.parent = SENTINEL; - - return; - } - - let yWasRed = (y.color === NodeColor.Red); - - if (y === y.parent.left) { - y.parent.left = x; - } else { - y.parent.right = x; - } - - if (y === z) { - x.parent = y.parent; - recomputeTreeMetadata(tree, x); - } else { - if (y.parent === z) { - x.parent = y; - } else { - x.parent = y.parent; - } - - // as we make changes to x's hierarchy, update size_left of subtree first - recomputeTreeMetadata(tree, x); - - y.left = z.left; - y.right = z.right; - y.parent = z.parent; - y.color = z.color; - - if (z === tree.root) { - tree.root = y; - } else { - if (z === z.parent.left) { - z.parent.left = y; - } else { - z.parent.right = y; - } - } - - if (y.left !== SENTINEL) { - y.left.parent = y; - } - if (y.right !== SENTINEL) { - y.right.parent = y; - } - // update metadata - // we replace z with y, so in this sub tree, the length change is z.item.length - y.size_left = z.size_left; - y.lf_left = z.lf_left; - recomputeTreeMetadata(tree, y); - } - - z.detach(); - - if (x.parent.left === x) { - let newSizeLeft = calculateSize(x); - let newLFLeft = calculateLF(x); - if (newSizeLeft !== x.parent.size_left || newLFLeft !== x.parent.lf_left) { - let delta = newSizeLeft - x.parent.size_left; - let lf_delta = newLFLeft - x.parent.lf_left; - x.parent.size_left = newSizeLeft; - x.parent.lf_left = newLFLeft; - updateTreeMetadata(tree, x.parent, delta, lf_delta); - } - } - - recomputeTreeMetadata(tree, x.parent); - - if (yWasRed) { - resetSentinel(); - return; - } - - // RB-DELETE-FIXUP - let w: TreeNode; - while (x !== tree.root && x.color === NodeColor.Black) { - if (x === x.parent.left) { - w = x.parent.right; - - if (w.color === NodeColor.Red) { - w.color = NodeColor.Black; - x.parent.color = NodeColor.Red; - leftRotate(tree, x.parent); - w = x.parent.right; - } - - if (w.left.color === NodeColor.Black && w.right.color === NodeColor.Black) { - w.color = NodeColor.Red; - x = x.parent; - } else { - if (w.right.color === NodeColor.Black) { - w.left.color = NodeColor.Black; - w.color = NodeColor.Red; - rightRotate(tree, w); - w = x.parent.right; - } - - w.color = x.parent.color; - x.parent.color = NodeColor.Black; - w.right.color = NodeColor.Black; - leftRotate(tree, x.parent); - x = tree.root; - } - } else { - w = x.parent.left; - - if (w.color === NodeColor.Red) { - w.color = NodeColor.Black; - x.parent.color = NodeColor.Red; - rightRotate(tree, x.parent); - w = x.parent.left; - } - - if (w.left.color === NodeColor.Black && w.right.color === NodeColor.Black) { - w.color = NodeColor.Red; - x = x.parent; - - } else { - if (w.left.color === NodeColor.Black) { - w.right.color = NodeColor.Black; - w.color = NodeColor.Red; - leftRotate(tree, w); - w = x.parent.left; - } - - w.color = x.parent.color; - x.parent.color = NodeColor.Black; - w.left.color = NodeColor.Black; - rightRotate(tree, x.parent); - x = tree.root; - } - } - } - x.color = NodeColor.Black; - resetSentinel(); -} - -export function fixInsert(tree: PieceTreeBase, x: TreeNode) { - recomputeTreeMetadata(tree, x); - - while (x !== tree.root && x.parent.color === NodeColor.Red) { - if (x.parent === x.parent.parent.left) { - const y = x.parent.parent.right; - - if (y.color === NodeColor.Red) { - x.parent.color = NodeColor.Black; - y.color = NodeColor.Black; - x.parent.parent.color = NodeColor.Red; - x = x.parent.parent; - } else { - if (x === x.parent.right) { - x = x.parent; - leftRotate(tree, x); - } - - x.parent.color = NodeColor.Black; - x.parent.parent.color = NodeColor.Red; - rightRotate(tree, x.parent.parent); - } - } else { - const y = x.parent.parent.left; - - if (y.color === NodeColor.Red) { - x.parent.color = NodeColor.Black; - y.color = NodeColor.Black; - x.parent.parent.color = NodeColor.Red; - x = x.parent.parent; - } else { - if (x === x.parent.left) { - x = x.parent; - rightRotate(tree, x); - } - x.parent.color = NodeColor.Black; - x.parent.parent.color = NodeColor.Red; - leftRotate(tree, x.parent.parent); - } - } - } - - tree.root.color = NodeColor.Black; -} - -export function updateTreeMetadata(tree: PieceTreeBase, x: TreeNode, delta: number, lineFeedCntDelta: number): void { - // node length change or line feed count change - while (x !== tree.root && x !== SENTINEL) { - if (x.parent.left === x) { - x.parent.size_left += delta; - x.parent.lf_left += lineFeedCntDelta; - } - - x = x.parent; - } -} - -export function recomputeTreeMetadata(tree: PieceTreeBase, x: TreeNode) { - let delta = 0; - let lf_delta = 0; - if (x === tree.root) { - return; - } - - if (delta === 0) { - // go upwards till the node whose left subtree is changed. - while (x !== tree.root && x === x.parent.right) { - x = x.parent; - } - - if (x === tree.root) { - // well, it means we add a node to the end (inorder) - return; - } - - // x is the node whose right subtree is changed. - x = x.parent; - - delta = calculateSize(x.left) - x.size_left; - lf_delta = calculateLF(x.left) - x.lf_left; - x.size_left += delta; - x.lf_left += lf_delta; - } - - // go upwards till root. O(logN) - while (x !== tree.root && (delta !== 0 || lf_delta !== 0)) { - if (x.parent.left === x) { - x.parent.size_left += delta; - x.parent.lf_left += lf_delta; - } - - x = x.parent; - } -} diff --git a/src/vs/editor/test/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.test.ts b/src/vs/editor/test/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.test.ts index fad324b8051..72d6b394b15 100644 --- a/src/vs/editor/test/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.test.ts +++ b/src/vs/editor/test/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.test.ts @@ -9,8 +9,7 @@ import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { PieceTreeTextBufferBuilder } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBufferBuilder'; import { DefaultEndOfLine } from 'vs/editor/common/model'; -import { PieceTreeBase } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase'; -import { SENTINEL, NodeColor, TreeNode } from 'vs/editor/common/model/pieceTreeTextBuffer/rbTreeBase'; +import { PieceTreeBase, SENTINEL, NodeColor, TreeNode } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase'; import { PieceTreeTextBuffer } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer'; const alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\r\n'; From 0533a8e6a0f13d7d1cfd105b04096c24797375d7 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Mon, 22 Jan 2018 20:42:49 -0800 Subject: [PATCH 091/128] Settings search - add setting to switch to new POST request format. And fix setting matcher for core settings from remote --- .../browser/preferencesRenderers.ts | 2 +- .../parts/preferences/common/preferences.ts | 1 + .../electron-browser/preferencesSearch.ts | 71 ++++++++++++------- 3 files changed, 46 insertions(+), 28 deletions(-) diff --git a/src/vs/workbench/parts/preferences/browser/preferencesRenderers.ts b/src/vs/workbench/parts/preferences/browser/preferencesRenderers.ts index c36f7249579..b92e3a6285a 100644 --- a/src/vs/workbench/parts/preferences/browser/preferencesRenderers.ts +++ b/src/vs/workbench/parts/preferences/browser/preferencesRenderers.ts @@ -17,7 +17,7 @@ import * as editorCommon from 'vs/editor/common/editorCommon'; import { Range, IRange } from 'vs/editor/common/core/range'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope, IConfigurationPropertySchema } from 'vs/platform/configuration/common/configurationRegistry'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IPreferencesService, ISettingsGroup, ISetting, IPreferencesEditorModel, IFilterResult, ISettingsEditorModel, IScoredResults, IWorkbenchSettingsConfiguration, IRemoteSetting, IExtensionSetting } from 'vs/workbench/parts/preferences/common/preferences'; +import { IPreferencesService, ISettingsGroup, ISetting, IPreferencesEditorModel, IFilterResult, ISettingsEditorModel, IScoredResults, IWorkbenchSettingsConfiguration, IExtensionSetting } from 'vs/workbench/parts/preferences/common/preferences'; import { SettingsEditorModel, DefaultSettingsEditorModel, WorkspaceConfigurationEditorModel } from 'vs/workbench/parts/preferences/common/preferencesModels'; import { ICodeEditor, IEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser'; import { IContextMenuService, ContextSubMenu } from 'vs/platform/contextview/browser/contextView'; diff --git a/src/vs/workbench/parts/preferences/common/preferences.ts b/src/vs/workbench/parts/preferences/common/preferences.ts index fc0212457ab..5157b9cefb8 100644 --- a/src/vs/workbench/parts/preferences/common/preferences.ts +++ b/src/vs/workbench/parts/preferences/common/preferences.ts @@ -24,6 +24,7 @@ export interface IWorkbenchSettingsConfiguration { naturalLanguageSearchEndpoint: string; naturalLanguageSearchKey: string; naturalLanguageSearchAutoIngestFeedback: boolean; + useNaturalLanguageSearchPost: boolean; enableNaturalLanguageSearch: boolean; enableNaturalLanguageSearchFeedback: boolean; } diff --git a/src/vs/workbench/parts/preferences/electron-browser/preferencesSearch.ts b/src/vs/workbench/parts/preferences/electron-browser/preferencesSearch.ts index 1c4fe18c3a3..b347d735afd 100644 --- a/src/vs/workbench/parts/preferences/electron-browser/preferencesSearch.ts +++ b/src/vs/workbench/parts/preferences/electron-browser/preferencesSearch.ts @@ -20,7 +20,6 @@ import { asJson } from 'vs/base/node/request'; import { Disposable } from 'vs/base/common/lifecycle'; import { IExtensionManagementService, LocalExtensionType, ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ILogService } from 'vs/platform/log/common/log'; -import { IStringDictionary } from 'vs/base/common/collections'; export interface IEndpointDetails { urlBase: string; @@ -69,8 +68,17 @@ export class PreferencesSearchService extends Disposable implements IPreferences } } - getRemoteSearchProvider(filter: string, newExtensionsOnly = false): RemoteSearchProvider { - return this.remoteSearchAllowed && this.instantiationService.createInstance(RemoteSearchProvider, filter, this._endpoint, this._installedExtensions, newExtensionsOnly); + getRemoteSearchProvider(filter: string, newExtensionsOnly = false): ISearchProvider { + const workbenchSettings = this.configurationService.getValue().workbench.settings; + + const opts: IRemoteSearchProviderOptions = { + filter, + newExtensionsOnly, + endpoint: this._endpoint, + usePost: workbenchSettings.useNaturalLanguageSearchPost + }; + + return this.remoteSearchAllowed && this.instantiationService.createInstance(RemoteSearchProvider, opts, this._installedExtensions); } getLocalSearchProvider(filter: string): LocalSearchProvider { @@ -115,20 +123,25 @@ export class LocalSearchProvider implements ISearchProvider { } } -export class RemoteSearchProvider implements ISearchProvider { - private _filter: string; +interface IRemoteSearchProviderOptions { + filter: string; + endpoint: IEndpointDetails; + newExtensionsOnly: boolean; + usePost: boolean; +} + +class RemoteSearchProvider implements ISearchProvider { private _remoteSearchP: TPromise; - constructor(filter: string, private endpoint: IEndpointDetails, private installedExtensions: TPromise, private newExtensionsOnly: boolean, + constructor(private options: IRemoteSearchProviderOptions, private installedExtensions: TPromise, @IEnvironmentService private environmentService: IEnvironmentService, @IRequestService private requestService: IRequestService, @ILogService private logService: ILogService ) { - this._filter = filter; - - this._remoteSearchP = filter ? - this.getSettingsFromBing(filter) : - TPromise.wrap(null); + this._remoteSearchP = (this.options.newExtensionsOnly && !this.options.usePost) ? TPromise.wrap(null) : + this.options.filter ? + this.getSettingsFromBing(this.options.filter) : + TPromise.wrap(null); } searchModel(preferencesModel: ISettingsEditorModel): TPromise { @@ -141,7 +154,7 @@ export class RemoteSearchProvider implements ISearchProvider { const highScoreKey = top(resultKeys, (a, b) => remoteResult.scoredResults[b].score - remoteResult.scoredResults[a].score, 1)[0]; const highScore = highScoreKey ? remoteResult.scoredResults[highScoreKey].score : 0; const minScore = highScore / 5; - if (this.newExtensionsOnly) { + if (this.options.newExtensionsOnly) { const passingScoreKeys = resultKeys.filter(k => remoteResult.scoredResults[k].score >= minScore); const filterMatches: ISettingMatch[] = passingScoreKeys.map(k => { const remoteSetting = remoteResult.scoredResults[k]; @@ -159,7 +172,7 @@ export class RemoteSearchProvider implements ISearchProvider { }; } else { const settingMatcher = this.getRemoteSettingMatcher(remoteResult.scoredResults, minScore, preferencesModel); - const filterMatches = preferencesModel.filterSettings(this._filter, group => null, settingMatcher); + const filterMatches = preferencesModel.filterSettings(this.options.filter, group => null, settingMatcher); return { filterMatches, metadata: remoteResult @@ -172,16 +185,19 @@ export class RemoteSearchProvider implements ISearchProvider { const start = Date.now(); return this.prepareRequest(filter).then(details => { this.logService.debug(`Searching settings via ${details.url}`); - this.logService.debug(`Body: ${details.body}`); + if (details.body) { + this.logService.debug(`Body: ${details.body}`); + } + const requestType = details.body ? 'post' : 'get'; return this.requestService.request({ - type: 'POST', + type: requestType, url: details.url, data: details.body, headers: { 'User-Agent': 'request', 'Content-Type': 'application/json; charset=utf-8', - 'api-key': this.endpoint.key + 'api-key': this.options.endpoint.key }, timeout: 5000 }).then(context => { @@ -197,12 +213,12 @@ export class RemoteSearchProvider implements ISearchProvider { .map(r => { const key = JSON.parse(r.setting || r.Setting); const packageId = r['packageid']; - const id = getSettingKey(packageId, key); + const id = getSettingKey(key, packageId); const packageName = r['packagename']; let extensionName: string; let extensionPublisher: string; - if (packageName.indexOf('##') >= 0) { + if (packageName && packageName.indexOf('##') >= 0) { [extensionPublisher, extensionName] = packageName.split('##'); } @@ -236,9 +252,9 @@ export class RemoteSearchProvider implements ISearchProvider { private getRemoteSettingMatcher(scoredResults: IScoredResults, minScore: number, preferencesModel: ISettingsEditorModel): ISettingMatcher { return (setting: ISetting, group: ISettingsGroup) => { - const remoteSetting = scoredResults[getSettingKey(group.id, setting.key)]; + const remoteSetting = scoredResults[getSettingKey(setting.key, group.id)] || scoredResults[getSettingKey(setting.key)]; if (remoteSetting && remoteSetting.score >= minScore) { - const settingMatches = new SettingMatches(this._filter, setting, false, (filter, setting) => preferencesModel.findValueMatches(filter, setting)).matches; + const settingMatches = new SettingMatches(this.options.filter, setting, false, (filter, setting) => preferencesModel.findValueMatches(filter, setting)).matches; return { matches: settingMatches, score: remoteSetting.score }; } @@ -255,16 +271,15 @@ export class RemoteSearchProvider implements ISearchProvider { query = query.replace(/\ +/g, '~ ') + '~'; const encodedQuery = encodeURIComponent(userQuery + ' || ' + query); - let url = `${this.endpoint.urlBase}?`; + let url = `${this.options.endpoint.urlBase}?`; const buildNumber = this.environmentService.settingsSearchBuildId; - if (this.endpoint.key) { + if (this.options.endpoint.key) { url += `${API_VERSION}&${QUERY_TYPE}`; } - const usePost = true; - if (usePost) { - const filters = this.newExtensionsOnly ? + if (this.options.usePost) { + const filters = this.options.newExtensionsOnly ? [`diminish eq 'latest'`] : await this.getVersionFilters(buildNumber); @@ -315,8 +330,10 @@ export class RemoteSearchProvider implements ISearchProvider { } } -function getSettingKey(packageId: string, name: string): string { - return packageId + '_' + name; +function getSettingKey(name: string, packageId?: string): string { + return packageId ? + packageId + '_' + name : + name; } const API_VERSION = 'api-version=2016-09-01-Preview'; From 758bfd87db9ddd706b9526f0ce4a298c73f6d074 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Mon, 22 Jan 2018 20:48:26 -0800 Subject: [PATCH 092/128] Fix issue with settings search match highlights disappearing --- .../preferences/common/preferencesModels.ts | 35 +++++++++---------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/src/vs/workbench/parts/preferences/common/preferencesModels.ts b/src/vs/workbench/parts/preferences/common/preferencesModels.ts index 044268407ba..95c7d255962 100644 --- a/src/vs/workbench/parts/preferences/common/preferencesModels.ts +++ b/src/vs/workbench/parts/preferences/common/preferencesModels.ts @@ -72,7 +72,22 @@ export abstract class AbstractSettingsModel extends EditorModel { } } - return filterMatches.sort((a, b) => b.score - a.score); + return filterMatches + .sort((a, b) => b.score - a.score) + .map(filteredMatch => { + // Fix match ranges to offset from setting start line + return { + setting: filteredMatch.setting, + score: filteredMatch.score, + matches: filteredMatch.matches && filteredMatch.matches.map(match => { + return new Range( + match.startLineNumber - filteredMatch.setting.range.startLineNumber, + match.startColumn, + match.endLineNumber - filteredMatch.setting.range.startLineNumber, + match.endColumn); + }) + }; + }); } public getPreference(key: string): ISetting { @@ -91,12 +106,9 @@ export abstract class AbstractSettingsModel extends EditorModel { private copySetting(setting: ISetting): ISetting { return { description: setting.description, - descriptionRanges: setting.descriptionRanges, key: setting.key, - keyRange: setting.keyRange, value: setting.value, range: setting.range, - valueRange: setting.valueRange, overrides: [], overrideOf: setting.overrideOf }; @@ -684,21 +696,6 @@ export class DefaultSettingsEditorModel extends AbstractSettingsModel implements } private writeSettingsGroupToBuilder(builder: SettingsContentBuilder, settingsGroup: ISettingsGroup, filterMatches: ISettingMatch[]): IRange[] { - // Fix match ranges to offset from setting start line - filterMatches = filterMatches.map(filteredMatch => { - return { - setting: filteredMatch.setting, - score: filteredMatch.score, - matches: filteredMatch.matches && filteredMatch.matches.map(match => { - return new Range( - match.startLineNumber - filteredMatch.setting.range.startLineNumber, - match.startColumn, - match.endLineNumber - filteredMatch.setting.range.startLineNumber, - match.endColumn); - }) - }; - }); - builder.pushGroup(settingsGroup); builder.pushLine(','); From d34a8edccce71d1f9fafaf6148c192501500f5da Mon Sep 17 00:00:00 2001 From: Peng Lyu Date: Mon, 22 Jan 2018 20:50:39 -0800 Subject: [PATCH 093/128] safe guard of search cache in buffer. --- .../pieceTreeTextBuffer.test.ts | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/src/vs/editor/test/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.test.ts b/src/vs/editor/test/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.test.ts index 72d6b394b15..a04ab200bd6 100644 --- a/src/vs/editor/test/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.test.ts +++ b/src/vs/editor/test/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.test.ts @@ -1530,4 +1530,62 @@ suite('buffer api', () => { assert(!a.equal(b)); }); +}); + +suite('search offset cache', () => { + test('render white space exception', () => { + let pieceTable = createTextBuffer(['class Name{\n\t\n\t\t\tget() {\n\n\t\t\t}\n\t\t}']); + let str = 'class Name{\n\t\n\t\t\tget() {\n\n\t\t\t}\n\t\t}'; + + pieceTable.insert(12, 's'); + str = str.substring(0, 12) + 's' + str.substring(12); + + pieceTable.insert(13, 'e'); + str = str.substring(0, 13) + 'e' + str.substring(13); + + pieceTable.insert(14, 't'); + str = str.substring(0, 14) + 't' + str.substring(14); + + pieceTable.insert(15, '()'); + str = str.substring(0, 15) + '()' + str.substring(15); + + pieceTable.delete(16, 1); + str = str.substring(0, 16) + str.substring(16 + 1); + + pieceTable.insert(17, '()'); + str = str.substring(0, 17) + '()' + str.substring(17); + + pieceTable.delete(18, 1); + str = str.substring(0, 18) + str.substring(18 + 1); + + pieceTable.insert(18, '}'); + str = str.substring(0, 18) + '}' + str.substring(18); + + pieceTable.insert(12, '\n'); + str = str.substring(0, 12) + '\n' + str.substring(12); + + pieceTable.delete(12, 1); + str = str.substring(0, 12) + str.substring(12 + 1); + + pieceTable.delete(18, 1); + str = str.substring(0, 18) + str.substring(18 + 1); + + pieceTable.insert(18, '}'); + str = str.substring(0, 18) + '}' + str.substring(18); + + pieceTable.delete(17, 2); + str = str.substring(0, 17) + str.substring(17 + 2); + + pieceTable.delete(16, 1); + str = str.substring(0, 16) + str.substring(16 + 1); + + pieceTable.insert(16, ')'); + str = str.substring(0, 16) + ')' + str.substring(16); + + pieceTable.delete(15, 2); + str = str.substring(0, 15) + str.substring(15 + 2); + + var content = pieceTable.getLinesRawContent(); + assert(content === str); + }); }); \ No newline at end of file From 95dfbe441038ed4cae61aae56c07c1a1f75b1b27 Mon Sep 17 00:00:00 2001 From: Peng Lyu Date: Mon, 22 Jan 2018 20:55:16 -0800 Subject: [PATCH 094/128] Move rbtree logic out of piecetreebase. --- .../pieceTreeTextBuffer/pieceTreeBase.ts | 456 +----------------- .../model/pieceTreeTextBuffer/rbTreeBase.ts | 427 ++++++++++++++++ .../pieceTreeTextBuffer.test.ts | 3 +- 3 files changed, 445 insertions(+), 441 deletions(-) create mode 100644 src/vs/editor/common/model/pieceTreeTextBuffer/rbTreeBase.ts diff --git a/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts b/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts index 9598964eff9..3c98e8b6b31 100644 --- a/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts +++ b/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts @@ -8,49 +8,7 @@ import { Position } from 'vs/editor/common/core/position'; import { CharCode } from 'vs/base/common/charCode'; import { Range } from 'vs/editor/common/core/range'; import { ITextSnapshot } from 'vs/platform/files/common/files'; - -export const enum NodeColor { - Black = 0, - Red = 1, -} - -export function getNodeColor(node: TreeNode) { - return node.color; -} - -function leftest(node: TreeNode): TreeNode { - while (node.left !== SENTINEL) { - node = node.left; - } - return node; -} - -function righttest(node: TreeNode): TreeNode { - while (node.right !== SENTINEL) { - node = node.right; - } - return node; -} - -function calculateSize(node: TreeNode): number { - if (node === SENTINEL) { - return 0; - } - - return node.size_left + node.piece.length + calculateSize(node.right); -} - -function calculateLF(node: TreeNode): number { - if (node === SENTINEL) { - return 0; - } - - return node.lf_left + node.piece.lineFeedCnt + calculateLF(node.right); -} - -function resetSentinel(): void { - SENTINEL.parent = SENTINEL; -} +import { leftest, righttest, updateTreeMetadata, rbDelete, fixInsert, NodeColor, SENTINEL, TreeNode } from 'vs/editor/common/model/pieceTreeTextBuffer/rbTreeBase'; // const lfRegex = new RegExp(/\r\n|\r|\n/g); @@ -138,84 +96,6 @@ export function createLineStarts(r: number[], str: string): LineStarts { return result; } -export class TreeNode { - parent: TreeNode; - left: TreeNode; - right: TreeNode; - color: NodeColor; - - // Piece - piece: Piece; - size_left: number; // size of the left subtree (not inorder) - lf_left: number; // line feeds cnt in the left subtree (not in order) - - constructor(piece: Piece, color: NodeColor) { - this.piece = piece; - this.color = color; - this.size_left = 0; - this.lf_left = 0; - this.parent = null; - this.left = null; - this.right = null; - } - - public next(): TreeNode { - if (this.right !== SENTINEL) { - return leftest(this.right); - } - - let node: TreeNode = this; - - while (node.parent !== SENTINEL) { - if (node.parent.left === node) { - break; - } - - node = node.parent; - } - - if (node.parent === SENTINEL) { - return SENTINEL; - } else { - return node.parent; - } - } - - public prev(): TreeNode { - if (this.left !== SENTINEL) { - return righttest(this.left); - } - - let node: TreeNode = this; - - while (node.parent !== SENTINEL) { - if (node.parent.right === node) { - break; - } - - node = node.parent; - } - - if (node.parent === SENTINEL) { - return SENTINEL; - } else { - return node.parent; - } - } - - public detach(): void { - this.parent = null; - this.left = null; - this.right = null; - } -} - -export const SENTINEL: TreeNode = new TreeNode(null, NodeColor.Black); -SENTINEL.parent = SENTINEL; -SENTINEL.left = SENTINEL; -SENTINEL.right = SENTINEL; -SENTINEL.color = NodeColor.Black; - export interface NodePosition { /** * Piece Index @@ -393,7 +273,6 @@ export class PieceTreeBase { this.create(chunks); } - // #region Buffer API public createSnapshot(BOM: string): ITextSnapshot { return new PieceTreeSnapshot(this, BOM); @@ -654,7 +533,7 @@ export class PieceTreeBase { if (startPosition.nodeStartOffset === offset) { if (cnt === startNode.piece.length) { // delete node let next = startNode.next(); - this.rbDelete(startNode); + rbDelete(this, startNode); this.validateCRLFWithPrevNode(next); this.computeBufferMetadata(); return; @@ -718,7 +597,7 @@ export class PieceTreeBase { piece.length -= 1; value += '\n'; - this.updateTreeMetadata(node, -1, -1); + updateTreeMetadata(this, node, -1, -1); if (node.piece.length === 0) { nodesToDel.push(node); @@ -822,7 +701,7 @@ export class PieceTreeBase { deleteNodes(nodes: TreeNode[]): void { for (let i = 0; i < nodes.length; i++) { - this.rbDelete(nodes[i]); + rbDelete(this, nodes[i]); } } @@ -978,7 +857,7 @@ export class PieceTreeBase { let lf_delta = piece.lineFeedCnt - originalLFCnt; let size_delta = newEndOffset - originalEndOffset; piece.length += size_delta; - this.updateTreeMetadata(node, size_delta, lf_delta); + updateTreeMetadata(this, node, size_delta, lf_delta); } deleteNodeHead(node: TreeNode, pos: BufferCursor) { @@ -992,7 +871,7 @@ export class PieceTreeBase { let lf_delta = piece.lineFeedCnt - originalLFCnt; let size_delta = originalStartOffset - newStartOffset; piece.length += size_delta; - this.updateTreeMetadata(node, size_delta, lf_delta); + updateTreeMetadata(this, node, size_delta, lf_delta); } shrinkNode(node: TreeNode, start: BufferCursor, end: BufferCursor) { @@ -1008,7 +887,7 @@ export class PieceTreeBase { let newLength = this.offsetInBuffer(piece.bufferIndex, start) - this.offsetInBuffer(piece.bufferIndex, originalStartPos); let newLFCnt = piece.lineFeedCnt; piece.length = newLength; - this.updateTreeMetadata(node, newLength - oldLength, newLFCnt - oldLFCnt); + updateTreeMetadata(this, node, newLength - oldLength, newLFCnt - oldLFCnt); // new right piece, end, originalEndPos let newPiece = new Piece( @@ -1052,7 +931,7 @@ export class PieceTreeBase { node.piece.lineFeedCnt = newLineFeedCnt; let lf_delta = newLineFeedCnt - oldLineFeedCnt; this._lastChangeBufferPos = endPos; - this.updateTreeMetadata(node, value.length, lf_delta); + updateTreeMetadata(this, node, value.length, lf_delta); } nodeAt(offset: number): NodePosition { @@ -1247,7 +1126,7 @@ export class PieceTreeBase { prev.piece.length -= 1; prev.piece.lineFeedCnt -= 1; - this.updateTreeMetadata(prev, - 1, -1); + updateTreeMetadata(this, prev, - 1, -1); if (prev.piece.length === 0) { nodesToDel.push(prev); } @@ -1259,7 +1138,7 @@ export class PieceTreeBase { next.piece.lineFeedCnt = this.getLineFeedCnt(next.piece.bufferIndex, next.piece.start, next.piece.end); // @todo, we can optimize // } - this.updateTreeMetadata(next, - 1, -1); + updateTreeMetadata(this, next, - 1, -1); if (next.piece.length === 0) { nodesToDel.push(next); } @@ -1270,7 +1149,7 @@ export class PieceTreeBase { // delete empty nodes for (let i = 0; i < nodesToDel.length; i++) { - this.rbDelete(nodesToDel[i]); + rbDelete(this, nodesToDel[i]); } } @@ -1282,7 +1161,7 @@ export class PieceTreeBase { value += '\n'; if (nextNode.piece.length === 1) { - this.rbDelete(nextNode); + rbDelete(this, nextNode); } else { let piece = nextNode.piece; @@ -1290,7 +1169,7 @@ export class PieceTreeBase { piece.start = newStart; piece.length -= 1; piece.lineFeedCnt = this.getLineFeedCnt(piece.bufferIndex, piece.start, piece.end); // @todo, we can optimize - this.updateTreeMetadata(nextNode, -1, -1); + updateTreeMetadata(this, nextNode, -1, -1); } return true; } @@ -1303,7 +1182,7 @@ export class PieceTreeBase { // #endregion - // #region Red Black Tree + // #region Tree operations iterate(node: TreeNode, callback: (node: TreeNode) => boolean): boolean { if (node === SENTINEL) { return callback(SENTINEL); @@ -1330,53 +1209,6 @@ export class PieceTreeBase { return currentContent; } - leftRotate(x: TreeNode) { - let y = x.right; - - // fix size_left - y.size_left += x.size_left + (x.piece ? x.piece.length : 0); - y.lf_left += x.lf_left + (x.piece ? x.piece.lineFeedCnt : 0); - x.right = y.left; - - if (y.left !== SENTINEL) { - y.left.parent = x; - } - y.parent = x.parent; - if (x.parent === SENTINEL) { - this.root = y; - } else if (x.parent.left === x) { - x.parent.left = y; - } else { - x.parent.right = y; - } - y.left = x; - x.parent = y; - } - - rightRotate(y: TreeNode) { - let x = y.left; - y.left = x.right; - if (x.right !== SENTINEL) { - x.right.parent = y; - } - x.parent = y.parent; - - // fix size_left - y.size_left -= x.size_left + (x.piece ? x.piece.length : 0); - y.lf_left -= x.lf_left + (x.piece ? x.piece.lineFeedCnt : 0); - - if (y.parent === SENTINEL) { - this.root = x; - } else if (y === y.parent.right) { - y.parent.right = x; - } else { - y.parent.left = x; - } - - x.right = y; - y.parent = x; - } - /** * node node * / \ / \ @@ -1405,7 +1237,7 @@ export class PieceTreeBase { z.parent = nextNode; } - this.fixInsert(z); + fixInsert(this, z); return z; } @@ -1437,266 +1269,10 @@ export class PieceTreeBase { z.parent = prevNode; } - this.fixInsert(z); + fixInsert(this, z); return z; } - rbDelete(z: TreeNode) { - let x: TreeNode; - let y: TreeNode; - - if (z.left === SENTINEL) { - y = z; - x = y.right; - } else if (z.right === SENTINEL) { - y = z; - x = y.left; - } else { - y = leftest(z.right); - x = y.right; - } - - if (y === this.root) { - this.root = x; - - // if x is null, we are removing the only node - x.color = NodeColor.Black; - z.detach(); - resetSentinel(); - this.root.parent = SENTINEL; - - return; - } - - let yWasRed = (y.color === NodeColor.Red); - - if (y === y.parent.left) { - y.parent.left = x; - } else { - y.parent.right = x; - } - - if (y === z) { - x.parent = y.parent; - this.recomputeTreeMetadata(x); - } else { - if (y.parent === z) { - x.parent = y; - } else { - x.parent = y.parent; - } - - // as we make changes to x's hierarchy, update size_left of subtree first - this.recomputeTreeMetadata(x); - - y.left = z.left; - y.right = z.right; - y.parent = z.parent; - y.color = z.color; - - if (z === this.root) { - this.root = y; - } else { - if (z === z.parent.left) { - z.parent.left = y; - } else { - z.parent.right = y; - } - } - - if (y.left !== SENTINEL) { - y.left.parent = y; - } - if (y.right !== SENTINEL) { - y.right.parent = y; - } - // update metadata - // we replace z with y, so in this sub tree, the length change is z.item.length - y.size_left = z.size_left; - y.lf_left = z.lf_left; - this.recomputeTreeMetadata(y); - } - - z.detach(); - - if (x.parent.left === x) { - let newSizeLeft = calculateSize(x); - let newLFLeft = calculateLF(x); - if (newSizeLeft !== x.parent.size_left || newLFLeft !== x.parent.lf_left) { - let delta = newSizeLeft - x.parent.size_left; - let lf_delta = newLFLeft - x.parent.lf_left; - x.parent.size_left = newSizeLeft; - x.parent.lf_left = newLFLeft; - this.updateTreeMetadata(x.parent, delta, lf_delta); - } - } - - this.recomputeTreeMetadata(x.parent); - - if (yWasRed) { - resetSentinel(); - return; - } - - // RB-DELETE-FIXUP - let w: TreeNode; - while (x !== this.root && x.color === NodeColor.Black) { - if (x === x.parent.left) { - w = x.parent.right; - - if (w.color === NodeColor.Red) { - w.color = NodeColor.Black; - x.parent.color = NodeColor.Red; - this.leftRotate(x.parent); - w = x.parent.right; - } - - if (w.left.color === NodeColor.Black && w.right.color === NodeColor.Black) { - w.color = NodeColor.Red; - x = x.parent; - } else { - if (w.right.color === NodeColor.Black) { - w.left.color = NodeColor.Black; - w.color = NodeColor.Red; - this.rightRotate(w); - w = x.parent.right; - } - - w.color = x.parent.color; - x.parent.color = NodeColor.Black; - w.right.color = NodeColor.Black; - this.leftRotate(x.parent); - x = this.root; - } - } else { - w = x.parent.left; - - if (w.color === NodeColor.Red) { - w.color = NodeColor.Black; - x.parent.color = NodeColor.Red; - this.rightRotate(x.parent); - w = x.parent.left; - } - - if (w.left.color === NodeColor.Black && w.right.color === NodeColor.Black) { - w.color = NodeColor.Red; - x = x.parent; - - } else { - if (w.left.color === NodeColor.Black) { - w.right.color = NodeColor.Black; - w.color = NodeColor.Red; - this.leftRotate(w); - w = x.parent.left; - } - - w.color = x.parent.color; - x.parent.color = NodeColor.Black; - w.left.color = NodeColor.Black; - this.rightRotate(x.parent); - x = this.root; - } - } - } - x.color = NodeColor.Black; - resetSentinel(); - } - - fixInsert(x: TreeNode) { - this.recomputeTreeMetadata(x); - - while (x !== this.root && x.parent.color === NodeColor.Red) { - if (x.parent === x.parent.parent.left) { - const y = x.parent.parent.right; - - if (y.color === NodeColor.Red) { - x.parent.color = NodeColor.Black; - y.color = NodeColor.Black; - x.parent.parent.color = NodeColor.Red; - x = x.parent.parent; - } else { - if (x === x.parent.right) { - x = x.parent; - this.leftRotate(x); - } - - x.parent.color = NodeColor.Black; - x.parent.parent.color = NodeColor.Red; - this.rightRotate(x.parent.parent); - } - } else { - const y = x.parent.parent.left; - - if (y.color === NodeColor.Red) { - x.parent.color = NodeColor.Black; - y.color = NodeColor.Black; - x.parent.parent.color = NodeColor.Red; - x = x.parent.parent; - } else { - if (x === x.parent.left) { - x = x.parent; - this.rightRotate(x); - } - x.parent.color = NodeColor.Black; - x.parent.parent.color = NodeColor.Red; - this.leftRotate(x.parent.parent); - } - } - } - - this.root.color = NodeColor.Black; - } - - updateTreeMetadata(x: TreeNode, delta: number, lineFeedCntDelta: number): void { - // node length change or line feed count change - while (x !== this.root && x !== SENTINEL) { - if (x.parent.left === x) { - x.parent.size_left += delta; - x.parent.lf_left += lineFeedCntDelta; - } - - x = x.parent; - } - } - - recomputeTreeMetadata(x: TreeNode) { - let delta = 0; - let lf_delta = 0; - if (x === this.root) { - return; - } - - if (delta === 0) { - // go upwards till the node whose left subtree is changed. - while (x !== this.root && x === x.parent.right) { - x = x.parent; - } - - if (x === this.root) { - // well, it means we add a node to the end (inorder) - return; - } - - // x is the node whose right subtree is changed. - x = x.parent; - - delta = calculateSize(x.left) - x.size_left; - lf_delta = calculateLF(x.left) - x.lf_left; - x.size_left += delta; - x.lf_left += lf_delta; - } - - // go upwards till root. O(logN) - while (x !== this.root && (delta !== 0 || lf_delta !== 0)) { - if (x.parent.left === x) { - x.parent.size_left += delta; - x.parent.lf_left += lf_delta; - } - - x = x.parent; - } - } - getContentOfSubTree(node: TreeNode): string { let str = ''; diff --git a/src/vs/editor/common/model/pieceTreeTextBuffer/rbTreeBase.ts b/src/vs/editor/common/model/pieceTreeTextBuffer/rbTreeBase.ts new file mode 100644 index 00000000000..84417b947cd --- /dev/null +++ b/src/vs/editor/common/model/pieceTreeTextBuffer/rbTreeBase.ts @@ -0,0 +1,427 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import { Piece, PieceTreeBase } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase'; + +export class TreeNode { + parent: TreeNode; + left: TreeNode; + right: TreeNode; + color: NodeColor; + + // Piece + piece: Piece; + size_left: number; // size of the left subtree (not inorder) + lf_left: number; // line feeds cnt in the left subtree (not in order) + + constructor(piece: Piece, color: NodeColor) { + this.piece = piece; + this.color = color; + this.size_left = 0; + this.lf_left = 0; + this.parent = null; + this.left = null; + this.right = null; + } + + public next(): TreeNode { + if (this.right !== SENTINEL) { + return leftest(this.right); + } + + let node: TreeNode = this; + + while (node.parent !== SENTINEL) { + if (node.parent.left === node) { + break; + } + + node = node.parent; + } + + if (node.parent === SENTINEL) { + return SENTINEL; + } else { + return node.parent; + } + } + + public prev(): TreeNode { + if (this.left !== SENTINEL) { + return righttest(this.left); + } + + let node: TreeNode = this; + + while (node.parent !== SENTINEL) { + if (node.parent.right === node) { + break; + } + + node = node.parent; + } + + if (node.parent === SENTINEL) { + return SENTINEL; + } else { + return node.parent; + } + } + + public detach(): void { + this.parent = null; + this.left = null; + this.right = null; + } +} + +export const SENTINEL: TreeNode = new TreeNode(null, NodeColor.Black); +SENTINEL.parent = SENTINEL; +SENTINEL.left = SENTINEL; +SENTINEL.right = SENTINEL; +SENTINEL.color = NodeColor.Black; + +export const enum NodeColor { + Black = 0, + Red = 1, +} + +export function leftest(node: TreeNode): TreeNode { + while (node.left !== SENTINEL) { + node = node.left; + } + return node; +} + +export function righttest(node: TreeNode): TreeNode { + while (node.right !== SENTINEL) { + node = node.right; + } + return node; +} + +export function calculateSize(node: TreeNode): number { + if (node === SENTINEL) { + return 0; + } + + return node.size_left + node.piece.length + calculateSize(node.right); +} + +export function calculateLF(node: TreeNode): number { + if (node === SENTINEL) { + return 0; + } + + return node.lf_left + node.piece.lineFeedCnt + calculateLF(node.right); +} + +export function resetSentinel(): void { + SENTINEL.parent = SENTINEL; +} + +export function leftRotate(tree: PieceTreeBase, x: TreeNode) { + let y = x.right; + + // fix size_left + y.size_left += x.size_left + (x.piece ? x.piece.length : 0); + y.lf_left += x.lf_left + (x.piece ? x.piece.lineFeedCnt : 0); + x.right = y.left; + + if (y.left !== SENTINEL) { + y.left.parent = x; + } + y.parent = x.parent; + if (x.parent === SENTINEL) { + tree.root = y; + } else if (x.parent.left === x) { + x.parent.left = y; + } else { + x.parent.right = y; + } + y.left = x; + x.parent = y; +} + +export function rightRotate(tree: PieceTreeBase, y: TreeNode) { + let x = y.left; + y.left = x.right; + if (x.right !== SENTINEL) { + x.right.parent = y; + } + x.parent = y.parent; + + // fix size_left + y.size_left -= x.size_left + (x.piece ? x.piece.length : 0); + y.lf_left -= x.lf_left + (x.piece ? x.piece.lineFeedCnt : 0); + + if (y.parent === SENTINEL) { + tree.root = x; + } else if (y === y.parent.right) { + y.parent.right = x; + } else { + y.parent.left = x; + } + + x.right = y; + y.parent = x; +} + +export function rbDelete(tree: PieceTreeBase, z: TreeNode) { + let x: TreeNode; + let y: TreeNode; + + if (z.left === SENTINEL) { + y = z; + x = y.right; + } else if (z.right === SENTINEL) { + y = z; + x = y.left; + } else { + y = leftest(z.right); + x = y.right; + } + + if (y === tree.root) { + tree.root = x; + + // if x is null, we are removing the only node + x.color = NodeColor.Black; + z.detach(); + resetSentinel(); + tree.root.parent = SENTINEL; + + return; + } + + let yWasRed = (y.color === NodeColor.Red); + + if (y === y.parent.left) { + y.parent.left = x; + } else { + y.parent.right = x; + } + + if (y === z) { + x.parent = y.parent; + recomputeTreeMetadata(tree, x); + } else { + if (y.parent === z) { + x.parent = y; + } else { + x.parent = y.parent; + } + + // as we make changes to x's hierarchy, update size_left of subtree first + recomputeTreeMetadata(tree, x); + + y.left = z.left; + y.right = z.right; + y.parent = z.parent; + y.color = z.color; + + if (z === tree.root) { + tree.root = y; + } else { + if (z === z.parent.left) { + z.parent.left = y; + } else { + z.parent.right = y; + } + } + + if (y.left !== SENTINEL) { + y.left.parent = y; + } + if (y.right !== SENTINEL) { + y.right.parent = y; + } + // update metadata + // we replace z with y, so in this sub tree, the length change is z.item.length + y.size_left = z.size_left; + y.lf_left = z.lf_left; + recomputeTreeMetadata(tree, y); + } + + z.detach(); + + if (x.parent.left === x) { + let newSizeLeft = calculateSize(x); + let newLFLeft = calculateLF(x); + if (newSizeLeft !== x.parent.size_left || newLFLeft !== x.parent.lf_left) { + let delta = newSizeLeft - x.parent.size_left; + let lf_delta = newLFLeft - x.parent.lf_left; + x.parent.size_left = newSizeLeft; + x.parent.lf_left = newLFLeft; + updateTreeMetadata(tree, x.parent, delta, lf_delta); + } + } + + recomputeTreeMetadata(tree, x.parent); + + if (yWasRed) { + resetSentinel(); + return; + } + + // RB-DELETE-FIXUP + let w: TreeNode; + while (x !== tree.root && x.color === NodeColor.Black) { + if (x === x.parent.left) { + w = x.parent.right; + + if (w.color === NodeColor.Red) { + w.color = NodeColor.Black; + x.parent.color = NodeColor.Red; + leftRotate(tree, x.parent); + w = x.parent.right; + } + + if (w.left.color === NodeColor.Black && w.right.color === NodeColor.Black) { + w.color = NodeColor.Red; + x = x.parent; + } else { + if (w.right.color === NodeColor.Black) { + w.left.color = NodeColor.Black; + w.color = NodeColor.Red; + rightRotate(tree, w); + w = x.parent.right; + } + + w.color = x.parent.color; + x.parent.color = NodeColor.Black; + w.right.color = NodeColor.Black; + leftRotate(tree, x.parent); + x = tree.root; + } + } else { + w = x.parent.left; + + if (w.color === NodeColor.Red) { + w.color = NodeColor.Black; + x.parent.color = NodeColor.Red; + rightRotate(tree, x.parent); + w = x.parent.left; + } + + if (w.left.color === NodeColor.Black && w.right.color === NodeColor.Black) { + w.color = NodeColor.Red; + x = x.parent; + + } else { + if (w.left.color === NodeColor.Black) { + w.right.color = NodeColor.Black; + w.color = NodeColor.Red; + leftRotate(tree, w); + w = x.parent.left; + } + + w.color = x.parent.color; + x.parent.color = NodeColor.Black; + w.left.color = NodeColor.Black; + rightRotate(tree, x.parent); + x = tree.root; + } + } + } + x.color = NodeColor.Black; + resetSentinel(); +} + +export function fixInsert(tree: PieceTreeBase, x: TreeNode) { + recomputeTreeMetadata(tree, x); + + while (x !== tree.root && x.parent.color === NodeColor.Red) { + if (x.parent === x.parent.parent.left) { + const y = x.parent.parent.right; + + if (y.color === NodeColor.Red) { + x.parent.color = NodeColor.Black; + y.color = NodeColor.Black; + x.parent.parent.color = NodeColor.Red; + x = x.parent.parent; + } else { + if (x === x.parent.right) { + x = x.parent; + leftRotate(tree, x); + } + + x.parent.color = NodeColor.Black; + x.parent.parent.color = NodeColor.Red; + rightRotate(tree, x.parent.parent); + } + } else { + const y = x.parent.parent.left; + + if (y.color === NodeColor.Red) { + x.parent.color = NodeColor.Black; + y.color = NodeColor.Black; + x.parent.parent.color = NodeColor.Red; + x = x.parent.parent; + } else { + if (x === x.parent.left) { + x = x.parent; + rightRotate(tree, x); + } + x.parent.color = NodeColor.Black; + x.parent.parent.color = NodeColor.Red; + leftRotate(tree, x.parent.parent); + } + } + } + + tree.root.color = NodeColor.Black; +} + +export function updateTreeMetadata(tree: PieceTreeBase, x: TreeNode, delta: number, lineFeedCntDelta: number): void { + // node length change or line feed count change + while (x !== tree.root && x !== SENTINEL) { + if (x.parent.left === x) { + x.parent.size_left += delta; + x.parent.lf_left += lineFeedCntDelta; + } + + x = x.parent; + } +} + +export function recomputeTreeMetadata(tree: PieceTreeBase, x: TreeNode) { + let delta = 0; + let lf_delta = 0; + if (x === tree.root) { + return; + } + + if (delta === 0) { + // go upwards till the node whose left subtree is changed. + while (x !== tree.root && x === x.parent.right) { + x = x.parent; + } + + if (x === tree.root) { + // well, it means we add a node to the end (inorder) + return; + } + + // x is the node whose right subtree is changed. + x = x.parent; + + delta = calculateSize(x.left) - x.size_left; + lf_delta = calculateLF(x.left) - x.lf_left; + x.size_left += delta; + x.lf_left += lf_delta; + } + + // go upwards till root. O(logN) + while (x !== tree.root && (delta !== 0 || lf_delta !== 0)) { + if (x.parent.left === x) { + x.parent.size_left += delta; + x.parent.lf_left += lf_delta; + } + + x = x.parent; + } +} diff --git a/src/vs/editor/test/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.test.ts b/src/vs/editor/test/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.test.ts index a04ab200bd6..a396f91cc40 100644 --- a/src/vs/editor/test/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.test.ts +++ b/src/vs/editor/test/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.test.ts @@ -9,7 +9,8 @@ import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { PieceTreeTextBufferBuilder } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBufferBuilder'; import { DefaultEndOfLine } from 'vs/editor/common/model'; -import { PieceTreeBase, SENTINEL, NodeColor, TreeNode } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase'; +import { PieceTreeBase } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase'; +import { SENTINEL, NodeColor, TreeNode } from 'vs/editor/common/model/pieceTreeTextBuffer/rbTreeBase'; import { PieceTreeTextBuffer } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer'; const alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\r\n'; From cdf4061d47b10ac38462bf6a4c5913f2b64c4104 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Mon, 22 Jan 2018 21:09:15 -0800 Subject: [PATCH 095/128] Prevent local settings search from searching setting descriptions --- .../electron-browser/preferencesSearch.ts | 32 +++++++++++-------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/src/vs/workbench/parts/preferences/electron-browser/preferencesSearch.ts b/src/vs/workbench/parts/preferences/electron-browser/preferencesSearch.ts index b347d735afd..f59e1ff3b7a 100644 --- a/src/vs/workbench/parts/preferences/electron-browser/preferencesSearch.ts +++ b/src/vs/workbench/parts/preferences/electron-browser/preferencesSearch.ts @@ -100,7 +100,7 @@ export class LocalSearchProvider implements ISearchProvider { let score = 1000; // Sort is not stable const settingMatcher = (setting: ISetting) => { - const matches = new SettingMatches(this._filter, setting, true, (filter, setting) => preferencesModel.findValueMatches(filter, setting)).matches; + const matches = new SettingMatches(this._filter, setting, true, false, (filter, setting) => preferencesModel.findValueMatches(filter, setting)).matches; return matches && matches.length ? { matches, @@ -254,7 +254,7 @@ class RemoteSearchProvider implements ISearchProvider { return (setting: ISetting, group: ISettingsGroup) => { const remoteSetting = scoredResults[getSettingKey(setting.key, group.id)] || scoredResults[getSettingKey(setting.key)]; if (remoteSetting && remoteSetting.score >= minScore) { - const settingMatches = new SettingMatches(this.options.filter, setting, false, (filter, setting) => preferencesModel.findValueMatches(filter, setting)).matches; + const settingMatches = new SettingMatches(this.options.filter, setting, false, false, (filter, setting) => preferencesModel.findValueMatches(filter, setting)).matches; return { matches: settingMatches, score: remoteSetting.score }; } @@ -369,7 +369,7 @@ class SettingMatches { public readonly matches: IRange[]; - constructor(searchString: string, setting: ISetting, private requireFullQueryMatch: boolean, private valuesMatcher: (filter: string, setting: ISetting) => IRange[]) { + constructor(searchString: string, setting: ISetting, private requireFullQueryMatch: boolean, private searchDescription, private valuesMatcher: (filter: string, setting: ISetting) => IRange[]) { this.matches = distinct(this._findMatchesInSetting(searchString, setting), (match) => `${match.startLineNumber}_${match.startColumn}_${match.endLineNumber}_${match.endColumn}_`); } @@ -377,7 +377,7 @@ class SettingMatches { const result = this._doFindMatchesInSetting(searchString, setting); if (setting.overrides && setting.overrides.length) { for (const subSetting of setting.overrides) { - const subSettingMatches = new SettingMatches(searchString, subSetting, this.requireFullQueryMatch, this.valuesMatcher); + const subSettingMatches = new SettingMatches(searchString, subSetting, this.requireFullQueryMatch, this.searchDescription, this.valuesMatcher); let words = searchString.split(' '); const descriptionRanges: IRange[] = this.getRangesForWords(words, this.descriptionMatchingWords, [subSettingMatches.descriptionMatchingWords, subSettingMatches.keyMatchingWords, subSettingMatches.valueMatchingWords]); const keyRanges: IRange[] = this.getRangesForWords(words, this.keyMatchingWords, [subSettingMatches.descriptionMatchingWords, subSettingMatches.keyMatchingWords, subSettingMatches.valueMatchingWords]); @@ -398,10 +398,12 @@ class SettingMatches { const settingKeyAsWords: string = setting.key.split('.').join(' '); for (const word of words) { - for (let lineIndex = 0; lineIndex < setting.description.length; lineIndex++) { - const descriptionMatches = matchesWords(word, setting.description[lineIndex], true); - if (descriptionMatches) { - this.descriptionMatchingWords.set(word, descriptionMatches.map(match => this.toDescriptionRange(setting, match, lineIndex))); + if (this.searchDescription) { + for (let lineIndex = 0; lineIndex < setting.description.length; lineIndex++) { + const descriptionMatches = matchesWords(word, setting.description[lineIndex], true); + if (descriptionMatches) { + this.descriptionMatchingWords.set(word, descriptionMatches.map(match => this.toDescriptionRange(setting, match, lineIndex))); + } } } @@ -419,12 +421,14 @@ class SettingMatches { } const descriptionRanges: IRange[] = []; - for (let lineIndex = 0; lineIndex < setting.description.length; lineIndex++) { - const matches = or(matchesContiguousSubString)(searchString, setting.description[lineIndex] || '') || []; - descriptionRanges.push(...matches.map(match => this.toDescriptionRange(setting, match, lineIndex))); - } - if (descriptionRanges.length === 0) { - descriptionRanges.push(...this.getRangesForWords(words, this.descriptionMatchingWords, [this.keyMatchingWords, this.valueMatchingWords])); + if (this.searchDescription) { + for (let lineIndex = 0; lineIndex < setting.description.length; lineIndex++) { + const matches = or(matchesContiguousSubString)(searchString, setting.description[lineIndex] || '') || []; + descriptionRanges.push(...matches.map(match => this.toDescriptionRange(setting, match, lineIndex))); + } + if (descriptionRanges.length === 0) { + descriptionRanges.push(...this.getRangesForWords(words, this.descriptionMatchingWords, [this.keyMatchingWords, this.valueMatchingWords])); + } } const keyMatches = or(matchesPrefix, matchesContiguousSubString)(searchString, setting.key); From 439ecdf3470762925ac4f632f1fd41af14ef14ef Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Mon, 22 Jan 2018 21:10:22 -0800 Subject: [PATCH 096/128] Fix core setting search matching with "post" request --- .../parts/preferences/electron-browser/preferencesSearch.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/parts/preferences/electron-browser/preferencesSearch.ts b/src/vs/workbench/parts/preferences/electron-browser/preferencesSearch.ts index f59e1ff3b7a..7614813e29f 100644 --- a/src/vs/workbench/parts/preferences/electron-browser/preferencesSearch.ts +++ b/src/vs/workbench/parts/preferences/electron-browser/preferencesSearch.ts @@ -252,7 +252,9 @@ class RemoteSearchProvider implements ISearchProvider { private getRemoteSettingMatcher(scoredResults: IScoredResults, minScore: number, preferencesModel: ISettingsEditorModel): ISettingMatcher { return (setting: ISetting, group: ISettingsGroup) => { - const remoteSetting = scoredResults[getSettingKey(setting.key, group.id)] || scoredResults[getSettingKey(setting.key)]; + const remoteSetting = scoredResults[getSettingKey(setting.key, group.id)] || // extension setting + scoredResults[getSettingKey(setting.key, 'core')] || // core setting + scoredResults[getSettingKey(setting.key)]; // core setting from original prod endpoint if (remoteSetting && remoteSetting.score >= minScore) { const settingMatches = new SettingMatches(this.options.filter, setting, false, false, (filter, setting) => preferencesModel.findValueMatches(filter, setting)).matches; return { matches: settingMatches, score: remoteSetting.score }; From 8c6fb54fb046df25363b4faac71884a7cb7b8a5c Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Mon, 22 Jan 2018 21:18:57 -0800 Subject: [PATCH 097/128] Remove unused import --- extensions/typescript/src/features/bufferSyncSupport.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/extensions/typescript/src/features/bufferSyncSupport.ts b/extensions/typescript/src/features/bufferSyncSupport.ts index f3b005cdb4b..186cd02ba6b 100644 --- a/extensions/typescript/src/features/bufferSyncSupport.ts +++ b/extensions/typescript/src/features/bufferSyncSupport.ts @@ -10,7 +10,6 @@ import * as Proto from '../protocol'; import { ITypeScriptServiceClient } from '../typescriptService'; import { Delayer } from '../utils/async'; import * as languageModeIds from '../utils/languageModeIds'; -import * as fileSchemes from '../utils/fileSchemes'; interface IDiagnosticRequestor { requestDiagnostic(filepath: string): void; From 6b9c28032a74431871df5e5d9df63eaa81cb0f23 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Mon, 22 Jan 2018 21:25:11 -0800 Subject: [PATCH 098/128] Settings search - Remove unused css rule --- .../parts/preferences/browser/media/preferences.css | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/vs/workbench/parts/preferences/browser/media/preferences.css b/src/vs/workbench/parts/preferences/browser/media/preferences.css index e3832b44473..04251e95105 100644 --- a/src/vs/workbench/parts/preferences/browser/media/preferences.css +++ b/src/vs/workbench/parts/preferences/browser/media/preferences.css @@ -276,13 +276,6 @@ cursor: pointer; } -.monaco-editor .newExtensionInstall { - background: url('info.svg') center center no-repeat; - width: 16px; - height: 16px; - cursor: pointer; -} - .monaco-editor .edit-preferences-widget.hidden { display: none; visibility: hidden; From 200e4013571881949f3c7196088c61f3752b1716 Mon Sep 17 00:00:00 2001 From: kieferrm Date: Mon, 22 Jan 2018 21:21:12 -0800 Subject: [PATCH 099/128] Revert "Use special prefix to tell TS that a resource is in-memory only (#42001)" This reverts commit 00f0f2c8962bfad8201d225cdb46646fb5f104f0. --- .../src/features/bufferSyncSupport.ts | 5 ++- .../typescript/src/typescriptServiceClient.ts | 37 ++++++------------- 2 files changed, 15 insertions(+), 27 deletions(-) diff --git a/extensions/typescript/src/features/bufferSyncSupport.ts b/extensions/typescript/src/features/bufferSyncSupport.ts index 186cd02ba6b..63f0bdfbc86 100644 --- a/extensions/typescript/src/features/bufferSyncSupport.ts +++ b/extensions/typescript/src/features/bufferSyncSupport.ts @@ -48,7 +48,10 @@ class SyncedBuffer { } if (this.client.apiVersion.has230Features()) { - args.projectRootPath = this.client.getWorkspaceRootForResource(this.document.uri); + const root = this.client.getWorkspaceRootForResource(this.document.uri); + if (root) { + args.projectRootPath = root; + } } if (this.client.apiVersion.has240Features()) { diff --git a/extensions/typescript/src/typescriptServiceClient.ts b/extensions/typescript/src/typescriptServiceClient.ts index a36818b3905..2901d184ba1 100644 --- a/extensions/typescript/src/typescriptServiceClient.ts +++ b/extensions/typescript/src/typescriptServiceClient.ts @@ -578,12 +578,12 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient } public normalizePath(resource: Uri): string | null { - if (this._apiVersion.has213Features()) { - if (resource.scheme === fileSchemes.walkThroughSnippet || resource.scheme === fileSchemes.untitled) { - const dirName = path.dirname(resource.path); - const fileName = this.inMemoryResourcePrefix + path.basename(resource.path); - return resource.with({ path: path.join(dirName, fileName) }).toString(true); - } + if (resource.scheme === fileSchemes.walkThroughSnippet) { + return resource.toString(); + } + + if (resource.scheme === fileSchemes.untitled && this._apiVersion.has213Features()) { + return resource.toString(); } if (resource.scheme !== fileSchemes.file) { @@ -599,24 +599,11 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient return result.replace(new RegExp('\\' + this.pathSeparator, 'g'), '/'); } - private get inMemoryResourcePrefix(): string { - return this._apiVersion.has270Features() ? '^' : ''; - } - public asUrl(filepath: string): Uri { - if (this._apiVersion.has213Features()) { - if (filepath.startsWith(TypeScriptServiceClient.WALK_THROUGH_SNIPPET_SCHEME_COLON) || (filepath.startsWith(fileSchemes.untitled + ':')) - ) { - let resource = Uri.parse(filepath); - if (this.inMemoryResourcePrefix) { - const dirName = path.dirname(resource.path); - const fileName = path.basename(resource.path); - if (fileName.startsWith(this.inMemoryResourcePrefix)) { - resource = resource.with({ path: path.join(dirName, fileName.slice(this.inMemoryResourcePrefix.length)) }); - } - } - return resource; - } + if (filepath.startsWith(TypeScriptServiceClient.WALK_THROUGH_SNIPPET_SCHEME_COLON) + || (filepath.startsWith(fileSchemes.untitled + ':') && this._apiVersion.has213Features()) + ) { + return Uri.parse(filepath); } return Uri.file(filepath); } @@ -633,10 +620,8 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient return root.uri.fsPath; } } - return roots[0].uri.fsPath; } - - return undefined; + return roots[0].uri.fsPath; } public execute(command: string, args: any, expectsResultOrToken?: boolean | CancellationToken): Promise { From 1502c8c2a007af5e3e1b85992766f546d9b94db6 Mon Sep 17 00:00:00 2001 From: kieferrm Date: Mon, 22 Jan 2018 22:26:32 -0800 Subject: [PATCH 100/128] update distro --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 965756d054b..dc5b1d8acfc 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.20.0", - "distro": "3e04fd7a1141705f5e82fb1175edc03d0d4ddf9c", + "distro": "2478cca5311e147817eef29cb81c0995a3517842", "author": { "name": "Microsoft Corporation" }, From a29d44c682505fa83bb6153fdb8ed8f29d7b0d0d Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 23 Jan 2018 07:31:13 +0100 Subject: [PATCH 101/128] fix #41940 --- .../browser/parts/editor/editorCommands.ts | 97 +++++++++++++------ .../browser/parts/editor/titleControl.ts | 6 +- src/vs/workbench/common/editor.ts | 10 ++ 3 files changed, 82 insertions(+), 31 deletions(-) diff --git a/src/vs/workbench/browser/parts/editor/editorCommands.ts b/src/vs/workbench/browser/parts/editor/editorCommands.ts index 42d2ed951c4..f31156a4099 100644 --- a/src/vs/workbench/browser/parts/editor/editorCommands.ts +++ b/src/vs/workbench/browser/parts/editor/editorCommands.ts @@ -8,7 +8,7 @@ import * as types from 'vs/base/common/types'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService'; -import { ActiveEditorMoveArguments, ActiveEditorMovePositioning, ActiveEditorMovePositioningBy, EditorCommands, TextCompareEditorVisible, EditorInput, IEditorIdentifier } from 'vs/workbench/common/editor'; +import { ActiveEditorMoveArguments, ActiveEditorMovePositioning, ActiveEditorMovePositioningBy, EditorCommands, TextCompareEditorVisible, EditorInput, IEditorIdentifier, IEditorCommandsContext } from 'vs/workbench/common/editor'; import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IEditor, Position, POSITIONS, Direction, IEditorInput } from 'vs/platform/editor/common/editor'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; @@ -190,10 +190,18 @@ function registerDiffEditorCommands(): void { weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), when: void 0, primary: void 0, - handler: (accessor) => { + handler: (accessor, resource, context: IEditorCommandsContext) => { const editorService = accessor.get(IWorkbenchEditorService); + const editorGroupService = accessor.get(IEditorGroupService); + + let editor: IEditor; + if (context) { + const position = positionAndInput(editorGroupService, editorService, context).position; + editor = editorService.getVisibleEditors()[position]; + } else { + editor = editorService.getActiveEditor(); + } - const editor = editorService.getActiveEditor(); if (editor instanceof TextDiffEditor) { const control = editor.getControl(); const isInlineMode = !control.renderSideBySide; @@ -262,15 +270,15 @@ function registerEditorCommands() { weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), when: void 0, primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_U), - handler: (accessor, resource: URI, editorContext: IEditorIdentifier) => { + handler: (accessor, resource: URI, context: IEditorIdentifier | IEditorCommandsContext) => { const editorGroupService = accessor.get(IEditorGroupService); const model = editorGroupService.getStacksModel(); const editorService = accessor.get(IWorkbenchEditorService); - const contexts = getMultiSelectedEditorContexts(editorContext, accessor.get(IListService)); + const contexts = getMultiSelectedEditorContexts(toEditorIdentifier(context, editorGroupService), accessor.get(IListService)); - let positionOne: { unmodifiedOnly: boolean } = undefined; - let positionTwo: { unmodifiedOnly: boolean } = undefined; - let positionThree: { unmodifiedOnly: boolean } = undefined; + let positionOne: { unmodifiedOnly: boolean } = void 0; + let positionTwo: { unmodifiedOnly: boolean } = void 0; + let positionThree: { unmodifiedOnly: boolean } = void 0; contexts.forEach(c => { switch (model.positionOfGroup(c.group)) { case Position.ONE: positionOne = { unmodifiedOnly: true }; break; @@ -288,10 +296,10 @@ function registerEditorCommands() { weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), when: void 0, primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_W), - handler: (accessor, resource: URI, editorContext: IEditorIdentifier) => { + handler: (accessor, resource: URI, context: IEditorIdentifier | IEditorCommandsContext) => { const editorGroupService = accessor.get(IEditorGroupService); const editorService = accessor.get(IWorkbenchEditorService); - const contexts = getMultiSelectedEditorContexts(editorContext, accessor.get(IListService)); + const contexts = getMultiSelectedEditorContexts(toEditorIdentifier(context, editorGroupService), accessor.get(IListService)); const distinctGroups = distinct(contexts.map(c => c.group)); if (distinctGroups.length) { @@ -312,11 +320,11 @@ function registerEditorCommands() { when: void 0, primary: KeyMod.CtrlCmd | KeyCode.KEY_W, win: { primary: KeyMod.CtrlCmd | KeyCode.F4, secondary: [KeyMod.CtrlCmd | KeyCode.KEY_W] }, - handler: (accessor, resource: URI, editorContext: IEditorIdentifier) => { + handler: (accessor, resource: URI, context: IEditorIdentifier | IEditorCommandsContext) => { const editorGroupService = accessor.get(IEditorGroupService); const editorService = accessor.get(IWorkbenchEditorService); - const contexts = getMultiSelectedEditorContexts(editorContext, accessor.get(IListService)); + const contexts = getMultiSelectedEditorContexts(toEditorIdentifier(context, editorGroupService), accessor.get(IListService)); const groups = distinct(contexts.map(context => context.group)); const editorsToClose = new Map(); @@ -326,7 +334,7 @@ function registerEditorCommands() { if (position >= 0) { editorsToClose.set(position, contexts.map(c => { if (group === c.group) { - let input = c ? c.editor : undefined; + let input = c ? c.editor : void 0; if (!input) { // Get Top Editor at Position @@ -339,7 +347,7 @@ function registerEditorCommands() { return input; } - return undefined; + return void 0; }).filter(input => !!input)); } }); @@ -365,10 +373,10 @@ function registerEditorCommands() { when: void 0, primary: void 0, mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_T }, - handler: (accessor, resource: URI, editorContext: IEditorIdentifier) => { + handler: (accessor, resource: URI, context: IEditorIdentifier | IEditorCommandsContext) => { const editorGroupService = accessor.get(IEditorGroupService); const editorService = accessor.get(IWorkbenchEditorService); - const contexts = getMultiSelectedEditorContexts(editorContext, accessor.get(IListService)); + const contexts = getMultiSelectedEditorContexts(toEditorIdentifier(context, editorGroupService), accessor.get(IListService)); const groups = distinct(contexts.map(context => context.group)); const editorsToClose = new Map(); @@ -378,7 +386,7 @@ function registerEditorCommands() { return c.editor; } - return undefined; + return void 0; }).filter(input => !!input); const toClose = group.getEditors().filter(input => inputsToSkip.indexOf(input) === -1); @@ -398,11 +406,11 @@ function registerEditorCommands() { weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), when: void 0, primary: void 0, - handler: (accessor, resource: URI, editorContext: IEditorIdentifier) => { + handler: (accessor, resource: URI, context: IEditorIdentifier | IEditorCommandsContext) => { const editorGroupService = accessor.get(IEditorGroupService); const editorService = accessor.get(IWorkbenchEditorService); - const { position, input } = positionAndInput(editorGroupService, editorService, editorContext); + const { position, input } = positionAndInput(editorGroupService, editorService, context); if (typeof position === 'number' && input) { return editorService.closeEditors(position, { except: input, direction: Direction.RIGHT }); @@ -417,11 +425,11 @@ function registerEditorCommands() { weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), when: void 0, primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.Enter), - handler: (accessor, resource: URI, editorContext: IEditorIdentifier) => { + handler: (accessor, resource: URI, context: IEditorIdentifier | IEditorCommandsContext) => { const editorGroupService = accessor.get(IEditorGroupService); const editorService = accessor.get(IWorkbenchEditorService); - const { position, input } = positionAndInput(editorGroupService, editorService, editorContext); + const { position, input } = positionAndInput(editorGroupService, editorService, context); if (typeof position === 'number' && input) { return editorGroupService.pinEditor(position, input); @@ -436,7 +444,7 @@ function registerEditorCommands() { weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), when: void 0, primary: void 0, - handler: (accessor, resource: URI, editorContext: IEditorIdentifier) => { + handler: (accessor, resource: URI, context: IEditorCommandsContext) => { const editorGroupService = accessor.get(IEditorGroupService); const editorService = accessor.get(IWorkbenchEditorService); const quickOpenService = accessor.get(IQuickOpenService); @@ -447,7 +455,7 @@ function registerEditorCommands() { return quickOpenService.show(NAVIGATE_ALL_EDITORS_GROUP_PREFIX); } - const { position } = positionAndInput(editorGroupService, editorService, editorContext); + const { position } = positionAndInput(editorGroupService, editorService, context); switch (position) { case Position.TWO: @@ -481,7 +489,10 @@ function registerEditorCommands() { }); } -function positionAndInput(editorGroupService: IEditorGroupService, editorService: IWorkbenchEditorService, editorContext?: IEditorIdentifier): { position: Position, input: IEditorInput } { +function positionAndInput(editorGroupService: IEditorGroupService, editorService: IWorkbenchEditorService, context?: IEditorIdentifier | IEditorCommandsContext): { position: Position, input: IEditorInput } { + + // Resolve from context + const editorContext = toEditorIdentifier(context, editorGroupService); let position = editorContext ? editorGroupService.getStacksModel().positionOfGroup(editorContext.group) : null; let input = editorContext ? editorContext.editor : null; @@ -496,14 +507,14 @@ function positionAndInput(editorGroupService: IEditorGroupService, editorService } export function getMultiSelectedEditorContexts(editorContext: IEditorIdentifier, listService: IListService): IEditorIdentifier[] { - const list = listService.lastFocusedList; - // Mapping for open editors view - const isEditorIdentifier = (element: any) => 'group' in element && 'editor' in element; - const elementToContext = (element: IEditorIdentifier | EditorGroup) => element instanceof EditorGroup ? { group: element, editor: undefined } : element; + const elementToContext = (element: IEditorIdentifier | EditorGroup) => element instanceof EditorGroup ? { group: element, editor: void 0 } : element; + // First check for a focused list to return the selected items from + const list = listService.lastFocusedList; if (list instanceof List && list.isDOMFocused()) { const selection = list.getSelectedElements(); const focus = list.getFocusedElements(); + // Only respect selection if it contains focused element if (focus.length && selection && selection.indexOf(focus[0]) >= 0) { return list.getSelectedElements().filter(e => e instanceof EditorGroup || isEditorIdentifier(e)).map(elementToContext); @@ -514,5 +525,35 @@ export function getMultiSelectedEditorContexts(editorContext: IEditorIdentifier, } } + // Otherwise go with passed in context return !!editorContext ? [editorContext] : []; } + +function isEditorIdentifier(object: any): object is IEditorIdentifier { + const identifier = object as IEditorIdentifier; + + return identifier && !!identifier.group && !!identifier.editor; +} + +function isEditorGroupContext(object: any): object is IEditorCommandsContext { + const context = object as IEditorCommandsContext; + + return context && typeof context.groupId === 'number'; +} + +function toEditorIdentifier(object: IEditorIdentifier | IEditorCommandsContext, editorGroupService: IEditorGroupService): IEditorIdentifier { + if (isEditorIdentifier(object)) { + return object as IEditorIdentifier; + } + + if (isEditorGroupContext(object)) { + const stacks = editorGroupService.getStacksModel(); + const group = stacks.getGroup(object.groupId); + return { + group, + editor: typeof object.editorIndex === 'number' ? group.getEditor(object.editorIndex) : void 0 + }; + } + + return void 0; +} \ No newline at end of file diff --git a/src/vs/workbench/browser/parts/editor/titleControl.ts b/src/vs/workbench/browser/parts/editor/titleControl.ts index c7054d7fb5b..a4dba3d6963 100644 --- a/src/vs/workbench/browser/parts/editor/titleControl.ts +++ b/src/vs/workbench/browser/parts/editor/titleControl.ts @@ -15,7 +15,7 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; import { RunOnceScheduler } from 'vs/base/common/async'; import arrays = require('vs/base/common/arrays'); -import { IEditorStacksModel, IEditorGroup, IEditorIdentifier, EditorInput, IStacksModelChangeEvent, toResource } from 'vs/workbench/common/editor'; +import { IEditorStacksModel, IEditorGroup, IEditorIdentifier, EditorInput, IStacksModelChangeEvent, toResource, IEditorCommandsContext } from 'vs/workbench/common/editor'; import { IActionItem, ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar'; import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -171,7 +171,7 @@ export abstract class TitleControl extends Themable implements ITitleAreaControl public setContext(group: IEditorGroup): void { this.context = group; - this.editorActionsToolbar.context = { group }; + this.editorActionsToolbar.context = { groupId: group ? group.id : void 0 } as IEditorCommandsContext; } public hasContext(): boolean { @@ -398,7 +398,7 @@ export abstract class TitleControl extends Themable implements ITitleAreaControl this.contextMenuService.showContextMenu({ getAnchor: () => anchor, getActions: () => TPromise.as(actions), - getActionsContext: () => identifier, + getActionsContext: () => ({ groupId: identifier.group.id, editorIndex: identifier.group.indexOf(identifier.editor) } as IEditorCommandsContext), getKeyBinding: (action) => this.getKeybinding(action), onHide: (cancel) => { diff --git a/src/vs/workbench/common/editor.ts b/src/vs/workbench/common/editor.ts index 03436fe0657..96b57287a71 100644 --- a/src/vs/workbench/common/editor.ts +++ b/src/vs/workbench/common/editor.ts @@ -788,6 +788,16 @@ export interface IEditorIdentifier { editor: IEditorInput; } +/** + * The editor commands context is used for editor commands (e.g. in the editor title) + * and we must ensure that the context is serializable because it potentially travels + * to the extension host! + */ +export interface IEditorCommandsContext { + groupId: GroupIdentifier; + editorIndex?: number; +} + export interface IEditorCloseEvent extends IEditorIdentifier { replaced: boolean; index: number; From 545fcb99989a54b249da73c559684f8276f45191 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 23 Jan 2018 07:59:57 +0100 Subject: [PATCH 102/128] linux - disable failing test --- src/vs/base/test/node/extfs/extfs.test.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/vs/base/test/node/extfs/extfs.test.ts b/src/vs/base/test/node/extfs/extfs.test.ts index 686934d9e01..f521abbfd01 100644 --- a/src/vs/base/test/node/extfs/extfs.test.ts +++ b/src/vs/base/test/node/extfs/extfs.test.ts @@ -16,6 +16,7 @@ import strings = require('vs/base/common/strings'); import extfs = require('vs/base/node/extfs'); import { onError } from 'vs/base/test/common/utils'; import { Readable } from 'stream'; +import { isLinux } from 'vs/base/common/platform'; const ignore = () => { }; @@ -376,7 +377,11 @@ suite('Extfs', () => { }); }); - test('pasero writeFileAndFlush (stream, error handling EACCES)', function (done: () => void) { + test('writeFileAndFlush (stream, error handling EACCES)', function (done: () => void) { + if (isLinux) { + return done(); // somehow this test fails on Linux in our TFS builds + } + const id = uuid.generateUuid(); const parentDir = path.join(os.tmpdir(), 'vsctests', id); const newDir = path.join(parentDir, 'extfs', id); From 60440c72461e96893766bce73d7bdf1f5136a19f Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 23 Jan 2018 08:01:49 +0100 Subject: [PATCH 103/128] reduce test pressure --- .../services/backup/test/node/backupFileService.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/services/backup/test/node/backupFileService.test.ts b/src/vs/workbench/services/backup/test/node/backupFileService.test.ts index 2720754abbb..ff58403e381 100644 --- a/src/vs/workbench/services/backup/test/node/backupFileService.test.ts +++ b/src/vs/workbench/services/backup/test/node/backupFileService.test.ts @@ -147,7 +147,7 @@ suite('BackupFileService', () => { }); test('text file (large file, ITextSnapshot)', function (done: () => void) { - const largeString = (new Array(100 * 1024)).join('Large String\n'); + const largeString = (new Array(10 * 1024)).join('Large String\n'); const model = TextModel.createFromString(largeString); service.backupResource(fooFile, model.createSnapshot()).then(() => { @@ -160,7 +160,7 @@ suite('BackupFileService', () => { }); test('untitled file (large file, ITextSnapshot)', function (done: () => void) { - const largeString = (new Array(100 * 1024)).join('Large String\n'); + const largeString = (new Array(10 * 1024)).join('Large String\n'); const model = TextModel.createFromString(largeString); service.backupResource(untitledFile, model.createSnapshot()).then(() => { From 0d025550f3817490aa6c8718ddc79acb4ca7bab4 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Tue, 23 Jan 2018 09:29:45 +0100 Subject: [PATCH 104/128] cleanup list alt key change --- src/vs/base/browser/ui/list/listPaging.ts | 8 +-- src/vs/base/browser/ui/list/listWidget.ts | 54 +++++++++------ src/vs/platform/list/browser/listService.ts | 68 +++++++++++-------- .../electron-browser/views/openEditorsView.ts | 5 +- 4 files changed, 79 insertions(+), 56 deletions(-) diff --git a/src/vs/base/browser/ui/list/listPaging.ts b/src/vs/base/browser/ui/list/listPaging.ts index 0c72f6ff5e3..2de68902c7c 100644 --- a/src/vs/base/browser/ui/list/listPaging.ts +++ b/src/vs/base/browser/ui/list/listPaging.ts @@ -7,7 +7,7 @@ import 'vs/css!./list'; import { IDisposable } from 'vs/base/common/lifecycle'; import { range } from 'vs/base/common/arrays'; import { IDelegate, IRenderer, IListEvent } from './list'; -import { List, IListCreationOptions, IListStyles, IListOptions } from './listWidget'; +import { List, IListStyles, IListOptions } from './listWidget'; import { IPagedModel } from 'vs/base/common/paging'; import Event, { mapEvent } from 'vs/base/common/event'; @@ -67,7 +67,7 @@ export class PagedList { container: HTMLElement, delegate: IDelegate, renderers: IPagedRenderer[], - options: IListCreationOptions = {} // TODO@Joao: should be IListOptions + options: IListOptions = {} ) { const pagedRenderers = renderers.map(r => new PagedRenderer>(r, () => this.model)); this.list = new List(container, delegate, pagedRenderers, options); @@ -181,8 +181,4 @@ export class PagedList { style(styles: IListStyles): void { this.list.style(styles); } - - updateOptions(options: IListOptions): void { - this.list.updateOptions(options); - } } \ No newline at end of file diff --git a/src/vs/base/browser/ui/list/listWidget.ts b/src/vs/base/browser/ui/list/listWidget.ts index 1ee43f1916c..d1c7b582008 100644 --- a/src/vs/base/browser/ui/list/listWidget.ts +++ b/src/vs/base/browser/ui/list/listWidget.ts @@ -265,7 +265,7 @@ class KeyboardController implements IDisposable { constructor( private list: List, private view: ListView, - options: IListCreationOptions + options: IListOptions ) { const multipleSelectionSupport = !(options.multipleSelectionSupport === false); this.disposables = []; @@ -344,9 +344,23 @@ class KeyboardController implements IDisposable { } } +export function isSelectionSingleChangeEvent(event: IListMouseEvent | IListTouchEvent): boolean { + return platform.isMacintosh ? event.browserEvent.metaKey : event.browserEvent.ctrlKey; +} + +export function isSelectionRangeChangeEvent(event: IListMouseEvent | IListTouchEvent): boolean { + return event.browserEvent.shiftKey; +} + +const DefaultMultipleSelectionContoller = { + isSelectionSingleChangeEvent, + isSelectionRangeChangeEvent +}; + class MouseController implements IDisposable { private multipleSelectionSupport: boolean; + private multipleSelectionController: IMultipleSelectionController | undefined; private didJustPressContextMenuKey: boolean = false; private disposables: IDisposable[] = []; @@ -384,9 +398,13 @@ class MouseController implements IDisposable { constructor( private list: List, private view: ListView, - private options: IListCreationOptions = {} + private options: IListOptions = {} ) { - this.multipleSelectionSupport = options.multipleSelectionSupport !== false; + this.multipleSelectionSupport = !(options.multipleSelectionSupport === false); + + if (this.multipleSelectionSupport) { + this.multipleSelectionController = options.multipleSelectionController || DefaultMultipleSelectionContoller; + } view.onMouseDown(this.onMouseDown, this, this.disposables); view.onMouseClick(this.onPointer, this, this.disposables); @@ -396,19 +414,19 @@ class MouseController implements IDisposable { Gesture.addTarget(view.domNode); } - updateOptions(options: IListOptions): void { - this.options.useAltAsMultiSelectModifier = options.useAltAsMultiSelectModifier; - } - private isSelectionSingleChangeEvent(event: IListMouseEvent | IListTouchEvent): boolean { - if (this.options.useAltAsMultiSelectModifier) { - return event.browserEvent.altKey; + if (this.multipleSelectionController) { + return this.multipleSelectionController.isSelectionSingleChangeEvent(event); } return platform.isMacintosh ? event.browserEvent.metaKey : event.browserEvent.ctrlKey; } private isSelectionRangeChangeEvent(event: IListMouseEvent | IListTouchEvent): boolean { + if (this.multipleSelectionController) { + return this.multipleSelectionController.isSelectionRangeChangeEvent(event); + } + return event.browserEvent.shiftKey; } @@ -500,11 +518,12 @@ class MouseController implements IDisposable { } } -export interface IListOptions { - useAltAsMultiSelectModifier?: boolean; +export interface IMultipleSelectionController { + isSelectionSingleChangeEvent(event: IListMouseEvent | IListTouchEvent): boolean; + isSelectionRangeChangeEvent(event: IListMouseEvent | IListTouchEvent): boolean; } -export interface IListCreationOptions extends IListViewOptions, IListStyles, IListOptions { +export interface IListOptions extends IListViewOptions, IListStyles { identityProvider?: IIdentityProvider; ariaLabel?: string; mouseSupport?: boolean; @@ -513,6 +532,7 @@ export interface IListCreationOptions extends IListViewOptions, IListStyles, keyboardSupport?: boolean; verticalScrollMode?: ScrollbarVisibility; multipleSelectionSupport?: boolean; + multipleSelectionController?: IMultipleSelectionController; } export interface IListStyles { @@ -545,7 +565,7 @@ const defaultStyles: IListStyles = { listDropBackground: Color.fromHex('#383B3D') }; -const DefaultOptions: IListCreationOptions = { +const DefaultOptions: IListOptions = { keyboardSupport: true, mouseSupport: true, multipleSelectionSupport: true @@ -722,7 +742,7 @@ export class List implements ISpliceable, IDisposable { container: HTMLElement, delegate: IDelegate, renderers: IRenderer[], - options: IListCreationOptions = DefaultOptions + options: IListOptions = DefaultOptions ) { const aria = new Aria(); this.focus = new FocusTrait(i => this.getElementDomId(i)); @@ -772,12 +792,6 @@ export class List implements ISpliceable, IDisposable { this.style(options); } - updateOptions(options: IListOptions): void { - if (this.mouseController) { - this.mouseController.updateOptions(options); - } - } - splice(start: number, deleteCount: number, elements: T[] = []): void { if (deleteCount === 0 && elements.length === 0) { return; diff --git a/src/vs/platform/list/browser/listService.ts b/src/vs/platform/list/browser/listService.ts index 1bba7f047a6..d283b200549 100644 --- a/src/vs/platform/list/browser/listService.ts +++ b/src/vs/platform/list/browser/listService.ts @@ -5,12 +5,12 @@ 'use strict'; import { ITree, ITreeConfiguration, ITreeOptions } from 'vs/base/parts/tree/browser/tree'; -import { List, IListCreationOptions } from 'vs/base/browser/ui/list/listWidget'; +import { List, IListOptions, isSelectionRangeChangeEvent, isSelectionSingleChangeEvent, IMultipleSelectionController } from 'vs/base/browser/ui/list/listWidget'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IDisposable, toDisposable, combinedDisposable, dispose } from 'vs/base/common/lifecycle'; import { IContextKeyService, IContextKey, RawContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { PagedList, IPagedRenderer } from 'vs/base/browser/ui/list/listPaging'; -import { IDelegate, IRenderer } from 'vs/base/browser/ui/list/list'; +import { IDelegate, IRenderer, IListMouseEvent, IListTouchEvent } from 'vs/base/browser/ui/list/list'; import { Tree } from 'vs/base/parts/tree/browser/treeImpl'; import { attachListStyler } from 'vs/platform/theme/common/styler'; import { IThemeService } from 'vs/platform/theme/common/themeService'; @@ -96,8 +96,25 @@ function createScopedContextKeyService(contextKeyService: IContextKeyService, wi export const multiSelectModifierSettingKey = 'workbench.multiSelectModifier'; -function useAltAsMultiSelectModifier(configurationService: IConfigurationService): { useAltAsMultiSelectModifier: boolean } { - return { useAltAsMultiSelectModifier: configurationService.getValue(multiSelectModifierSettingKey) === 'alt' }; +export function useAltAsMultipleSelectionModifier(configurationService: IConfigurationService): boolean { + return configurationService.getValue(multiSelectModifierSettingKey) === 'alt'; +} + +class MultipleSelectionController implements IMultipleSelectionController { + + constructor(private configurationService: IConfigurationService) { } + + isSelectionSingleChangeEvent(event: IListMouseEvent | IListTouchEvent): boolean { + if (useAltAsMultipleSelectionModifier(this.configurationService)) { + return event.browserEvent.altKey; + } + + return isSelectionSingleChangeEvent(event); + } + + isSelectionRangeChangeEvent(event: IListMouseEvent | IListTouchEvent): boolean { + return isSelectionRangeChangeEvent(event); + } } export class WorkbenchList extends List { @@ -109,34 +126,28 @@ export class WorkbenchList extends List { container: HTMLElement, delegate: IDelegate, renderers: IRenderer[], - private options: IListCreationOptions, + options: IListOptions, @IContextKeyService contextKeyService: IContextKeyService, @IListService listService: IListService, @IThemeService themeService: IThemeService, @IConfigurationService configurationService: IConfigurationService ) { - super(container, delegate, renderers, mixin(options, useAltAsMultiSelectModifier(configurationService))); + const multipleSelectionSupport = !(options.multipleSelectionSupport === false); + + if (multipleSelectionSupport && !options.multipleSelectionController) { + options.multipleSelectionController = new MultipleSelectionController(configurationService); + } + + super(container, delegate, renderers, mixin(options, useAltAsMultipleSelectionModifier(configurationService))); this.contextKeyService = createScopedContextKeyService(contextKeyService, this); this.listDoubleSelection = WorkbenchListDoubleSelection.bindTo(this.contextKeyService); this.disposables.push(combinedDisposable([ this.contextKeyService, (listService as ListService).register(this), - attachListStyler(this, themeService) + attachListStyler(this, themeService), + this.onSelectionChange(() => this.listDoubleSelection.set(this.getSelection().length === 2)) ])); - this.disposables.push(this.onSelectionChange(() => { - const selection = this.getSelection(); - this.listDoubleSelection.set(selection && selection.length === 2); - })); - this.disposables.push(configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(multiSelectModifierSettingKey)) { - this.updateOptions(useAltAsMultiSelectModifier(configurationService)); - } - })); - } - - public get useAltAsMultiSelectModifier(): boolean { - return this.options.useAltAsMultiSelectModifier; } } @@ -149,24 +160,25 @@ export class WorkbenchPagedList extends PagedList { container: HTMLElement, delegate: IDelegate, renderers: IPagedRenderer[], - options: IListCreationOptions, + options: IListOptions, @IContextKeyService contextKeyService: IContextKeyService, @IListService listService: IListService, @IThemeService themeService: IThemeService, @IConfigurationService configurationService: IConfigurationService ) { - super(container, delegate, renderers, mixin(options, useAltAsMultiSelectModifier(configurationService))); + const multipleSelectionSupport = !(options.multipleSelectionSupport === false); + + if (multipleSelectionSupport && !options.multipleSelectionController) { + options.multipleSelectionController = new MultipleSelectionController(configurationService); + } + + super(container, delegate, renderers, mixin(options, useAltAsMultipleSelectionModifier(configurationService))); this.contextKeyService = createScopedContextKeyService(contextKeyService, this); this.disposable = combinedDisposable([ this.contextKeyService, (listService as ListService).register(this), - attachListStyler(this, themeService), - configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(multiSelectModifierSettingKey)) { - this.updateOptions(useAltAsMultiSelectModifier(configurationService)); - } - }) + attachListStyler(this, themeService) ]); } diff --git a/src/vs/workbench/parts/files/electron-browser/views/openEditorsView.ts b/src/vs/workbench/parts/files/electron-browser/views/openEditorsView.ts index 2c882a9221b..d411f925e09 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/openEditorsView.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/openEditorsView.ts @@ -28,7 +28,7 @@ import { EditorGroup } from 'vs/workbench/common/editor/editorStacksModel'; import { attachStylerCallback } from 'vs/platform/theme/common/styler'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { badgeBackground, badgeForeground, contrastBorder } from 'vs/platform/theme/common/colorRegistry'; -import { WorkbenchList } from 'vs/platform/list/browser/listService'; +import { WorkbenchList, useAltAsMultipleSelectionModifier } from 'vs/platform/list/browser/listService'; import { IDelegate, IRenderer, IListContextMenuEvent, IListMouseEvent } from 'vs/base/browser/ui/list/list'; import { EditorLabel } from 'vs/workbench/browser/labels'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; @@ -278,7 +278,8 @@ export class OpenEditorsView extends ViewsViewletPanel { const position = this.model.positionOfGroup(element.group); this.editorService.closeEditor(position, element.editor).done(null, errors.onUnexpectedError); } else { - this.openEditor(element, { preserveFocus: !isDoubleClick, pinned: isDoubleClick, sideBySide: this.list.useAltAsMultiSelectModifier ? (event.browserEvent.ctrlKey || event.browserEvent.metaKey) : event.browserEvent.altKey }); + const sideBySide = useAltAsMultipleSelectionModifier(this.configurationService) ? event.browserEvent.altKey : (event.browserEvent.ctrlKey || event.browserEvent.metaKey); + this.openEditor(element, { preserveFocus: !isDoubleClick, pinned: isDoubleClick, sideBySide }); } } From f7cde7af9c25e8553453197e40d9f87e0eb60ddf Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Tue, 23 Jan 2018 09:37:50 +0100 Subject: [PATCH 105/128] fix bad state expectation --- src/vs/platform/update/electron-main/abstractUpdateService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/platform/update/electron-main/abstractUpdateService.ts b/src/vs/platform/update/electron-main/abstractUpdateService.ts index fcc47b9f7a6..04e3ccdfeb3 100644 --- a/src/vs/platform/update/electron-main/abstractUpdateService.ts +++ b/src/vs/platform/update/electron-main/abstractUpdateService.ts @@ -120,7 +120,7 @@ export abstract class AbstractUpdateService implements IUpdateService { applyUpdate(): TPromise { this.logService.trace('update#applyUpdate, state = ', this.state.type); - if (this.state.type !== StateType.Ready) { + if (this.state.type !== StateType.Downloaded) { return TPromise.as(null); } From 3359b50c4d5b0fb0f2dbe29e18c5ec18e3f8b820 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Tue, 23 Jan 2018 10:09:12 +0100 Subject: [PATCH 106/128] use windowsVerbatimArguments --- src/typings/node.d.ts | 1 + src/vs/platform/update/electron-main/updateService.win32.ts | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/typings/node.d.ts b/src/typings/node.d.ts index 37db4dd80f0..4fa18421982 100644 --- a/src/typings/node.d.ts +++ b/src/typings/node.d.ts @@ -1720,6 +1720,7 @@ declare module "child_process" { uid?: number; gid?: number; shell?: boolean | string; + windowsVerbatimArguments?: boolean; } export function spawn(command: string, args?: string[], options?: SpawnOptions): ChildProcess; diff --git a/src/vs/platform/update/electron-main/updateService.win32.ts b/src/vs/platform/update/electron-main/updateService.win32.ts index b50b3ac33b6..ef20367626c 100644 --- a/src/vs/platform/update/electron-main/updateService.win32.ts +++ b/src/vs/platform/update/electron-main/updateService.win32.ts @@ -172,7 +172,8 @@ export class Win32UpdateService extends AbstractUpdateService { return pfs.writeFile(this.availableUpdate.updateFilePath, 'flag').then(() => { const child = spawn(this.availableUpdate.packagePath, ['/verysilent', `/update="${this.availableUpdate.updateFilePath}"`, '/nocloseapplications', '/mergetasks=runcode,!desktopicon,!quicklaunchicon'], { detached: true, - stdio: ['ignore', 'ignore', 'ignore'] + stdio: ['ignore', 'ignore', 'ignore'], + windowsVerbatimArguments: true }); child.once('exit', () => { From 3ecef8c1dd1d23afdab60ee8a5ff0b7006498810 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Tue, 23 Jan 2018 10:17:51 +0100 Subject: [PATCH 107/128] add 'configuration' (read uri/path) of workspace config file, #41408 --- src/vs/platform/workspace/common/workspace.ts | 2 +- src/vs/workbench/api/node/extHost.protocol.ts | 1 + src/vs/workbench/api/node/extHostExtensionService.ts | 2 ++ 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/vs/platform/workspace/common/workspace.ts b/src/vs/platform/workspace/common/workspace.ts index af594644bae..1493935a013 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 }; + return { id: this.id, folders: this.folders, name: this.name, configuration: this.configuration }; } } diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index d4dcd03d363..faacf07efa7 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -71,6 +71,7 @@ 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 d39a58a432e..73a8df57d4e 100644 --- a/src/vs/workbench/api/node/extHostExtensionService.ts +++ b/src/vs/workbench/api/node/extHostExtensionService.ts @@ -22,6 +22,7 @@ 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 { @@ -108,6 +109,7 @@ 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) ); From c425644317ca8847b5a22aaea2e58c4553f5c38c Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 23 Jan 2018 12:20:53 +0300 Subject: [PATCH 108/128] Revert - move striping logic back to javascript for running extensions --- .../electron-browser/media/runtimeExtensionsEditor.css | 2 +- .../extensions/electron-browser/runtimeExtensionsEditor.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/parts/extensions/electron-browser/media/runtimeExtensionsEditor.css b/src/vs/workbench/parts/extensions/electron-browser/media/runtimeExtensionsEditor.css index bad1b92e994..8a9a44e97df 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/media/runtimeExtensionsEditor.css +++ b/src/vs/workbench/parts/extensions/electron-browser/media/runtimeExtensionsEditor.css @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.runtime-extensions-editor .monaco-list .monaco-list-rows > .monaco-list-row:nth-child(even):not(:hover):not(.focused) { +.runtime-extensions-editor .monaco-list .monaco-list-rows > .monaco-list-row.odd:not(:hover):not(.focused) { background-color: rgba(130, 130, 130, 0.08); } diff --git a/src/vs/workbench/parts/extensions/electron-browser/runtimeExtensionsEditor.ts b/src/vs/workbench/parts/extensions/electron-browser/runtimeExtensionsEditor.ts index 23c2fa6549b..bc300591ee4 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/runtimeExtensionsEditor.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/runtimeExtensionsEditor.ts @@ -291,6 +291,8 @@ export class RuntimeExtensionsEditor extends BaseEditor { data.elementDisposables = dispose(data.elementDisposables); + toggleClass(data.root, 'odd', index % 2 === 1); + data.name.textContent = element.marketplaceInfo ? element.marketplaceInfo.displayName : element.description.displayName; const activationTimes = element.status.activationTimes; From 67880b353d91014356a7648791ea32fe55abfba1 Mon Sep 17 00:00:00 2001 From: isidor Date: Tue, 23 Jan 2018 10:41:19 +0100 Subject: [PATCH 109/128] debug: introduce WorkspaceLaunch --- .../parts/debug/browser/debugActionItems.ts | 6 +- .../parts/debug/browser/debugActions.ts | 2 +- .../parts/debug/browser/debugQuickOpen.ts | 8 +- .../parts/debug/browser/debugStatus.ts | 2 +- src/vs/workbench/parts/debug/common/debug.ts | 5 ++ .../debug/electron-browser/debugCommands.ts | 2 +- .../debugConfigurationManager.ts | 74 ++++++++++++++++--- .../debug/electron-browser/debugService.ts | 25 +++++-- 8 files changed, 97 insertions(+), 27 deletions(-) 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..562bae5c743 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 || p.session.root.uri.toString() === launch.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..8b5a6174a64 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; /** diff --git a/src/vs/workbench/parts/debug/electron-browser/debugCommands.ts b/src/vs/workbench/parts/debug/electron-browser/debugCommands.ts index af425b59ffb..e033c2cdeca 100644 --- a/src/vs/workbench/parts/debug/electron-browser/debugCommands.ts +++ b/src/vs/workbench/parts/debug/electron-browser/debugCommands.ts @@ -169,7 +169,7 @@ export function registerCommands(): void { 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() === workspaceUri).pop() || manager.selectedLaunch; return launch.openConfigFile(false).done(result => { if (result.editor && !result.configFileCreated) { diff --git a/src/vs/workbench/parts/debug/electron-browser/debugConfigurationManager.ts b/src/vs/workbench/parts/debug/electron-browser/debugConfigurationManager.ts index 26594a28da5..2a067455762 100644 --- a/src/vs/workbench/parts/debug/electron-browser/debugConfigurationManager.ts +++ b/src/vs/workbench/parts/debug/electron-browser/debugConfigurationManager.ts @@ -24,7 +24,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'; @@ -231,7 +231,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)); } @@ -334,6 +334,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; } @@ -355,6 +359,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; @@ -437,7 +449,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); } } @@ -452,15 +464,27 @@ class Launch implements ILaunch { private configurationManager: ConfigurationManager, public workspace: IWorkspaceFolder, @IFileService private fileService: IFileService, - @IWorkbenchEditorService private editorService: IWorkbenchEditorService, - @IConfigurationService private configurationService: IConfigurationService, + @IWorkbenchEditorService protected editorService: IWorkbenchEditorService, + @IConfigurationService protected configurationService: IConfigurationService, @IConfigurationResolverService private configurationResolverService: IConfigurationResolverService ) { // 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; } @@ -469,7 +493,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 { @@ -486,7 +510,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; } @@ -517,10 +541,6 @@ 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; }> { const resource = this.uri; let configFileCreated = false; @@ -576,3 +596,33 @@ class Launch implements ILaunch { }); } } + +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 + ) { + super(configurationManager, undefined, fileService, editorService, configurationService, configurationResolverService); + } + + 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<{ editor: IEditor; configFileCreated: boolean; }, any> { + return this.editorService.openEditor({ resource: this.workspaceContextService.getWorkspace().configuration }).then(editor => ({ editor, configFileCreated: false })); + } +} diff --git a/src/vs/workbench/parts/debug/electron-browser/debugService.ts b/src/vs/workbench/parts/debug/electron-browser/debugService.ts index b2434227ac5..288a5760b2f 100644 --- a/src/vs/workbench/parts/debug/electron-browser/debugService.ts +++ b/src/vs/workbench/parts/debug/electron-browser/debugService.ts @@ -668,8 +668,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.uri.toString() === root.uri.toString()).pop() + : this.configurationManager.getWorkspaceLaunch(); let config: debug.IConfig, compound: debug.ICompound; if (!configOrName) { @@ -683,7 +683,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 +692,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 TPromise.join(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 +738,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.uri : undefined, type, config).then(config => { // a falsy config indicates an aborted launch if (config && config.type) { return this.createProcess(root, config, sessionId); From 802be774a72f0e53a2c96c208a93b7a68df9fe31 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Tue, 23 Jan 2018 10:50:09 +0100 Subject: [PATCH 110/128] #36967 - Support reading workspace launches in mulit root workspace --- .../common/configurationModels.ts | 43 ++++++++++---- .../configuration/node/configuration.ts | 41 +++++++------ .../node/configurationService.ts | 2 +- .../test/node/configurationService.test.ts | 59 ++++++++++++++++--- 4 files changed, 105 insertions(+), 40 deletions(-) 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/configurationService.ts b/src/vs/workbench/services/configuration/node/configurationService.ts index d5c3af13b8b..4e3b25efb56 100644 --- a/src/vs/workbench/services/configuration/node/configurationService.ts +++ b/src/vs/workbench/services/configuration/node/configurationService.ts @@ -279,7 +279,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 d41ac767a69..1989f5e6baf 100644 --- a/src/vs/workbench/services/configuration/test/node/configurationService.test.ts +++ b/src/vs/workbench/services/configuration/test/node/configurationService.test.ts @@ -1038,6 +1038,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')); @@ -1102,13 +1152,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); - }); - }); }); From 4f5b23ef527648d84439bb8b2569115ae582deca Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Tue, 23 Jan 2018 10:51:25 +0100 Subject: [PATCH 111/128] update: should not show progress if user quits code --- build/win32/code.iss | 23 ++++++++++++++++++----- build/win32/inno_updater.exe | Bin 180224 -> 181248 bytes 2 files changed, 18 insertions(+), 5 deletions(-) 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 d82b1430c6407c1bf6a21418629564ddd599956e..6b8c7a981423d8c8c8cd66b1389b5864d2731d41 100644 GIT binary patch delta 46572 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 delta 45689 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%0D*3D?v$J&WDNMUHx?kiBr#VWaF+ARQJ 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 From 82532abe87d0a9b67ee8cb2aa48d6e3992a965ae Mon Sep 17 00:00:00 2001 From: isidor Date: Tue, 23 Jan 2018 10:59:27 +0100 Subject: [PATCH 112/128] debug: simplify openConfigFile --- src/vs/workbench/parts/debug/common/debug.ts | 2 +- .../debug/electron-browser/debugCommands.ts | 6 ++-- .../debugConfigurationManager.ts | 34 ++++++++++++------- .../debug/electron-browser/debugService.ts | 7 +--- 4 files changed, 27 insertions(+), 22 deletions(-) diff --git a/src/vs/workbench/parts/debug/common/debug.ts b/src/vs/workbench/parts/debug/common/debug.ts index 8b5a6174a64..8356f23b448 100644 --- a/src/vs/workbench/parts/debug/common/debug.ts +++ b/src/vs/workbench/parts/debug/common/debug.ts @@ -471,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/electron-browser/debugCommands.ts b/src/vs/workbench/parts/debug/electron-browser/debugCommands.ts index e033c2cdeca..c0610e8e9a9 100644 --- a/src/vs/workbench/parts/debug/electron-browser/debugCommands.ts +++ b/src/vs/workbench/parts/debug/electron-browser/debugCommands.ts @@ -171,9 +171,9 @@ export function registerCommands(): void { } const launch = manager.getLaunches().filter(l => l.uri.toString() === workspaceUri).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 2a067455762..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'; @@ -34,6 +35,7 @@ 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', [], { @@ -466,7 +468,8 @@ class Launch implements ILaunch { @IFileService private fileService: IFileService, @IWorkbenchEditorService protected editorService: IWorkbenchEditorService, @IConfigurationService protected configurationService: IConfigurationService, - @IConfigurationResolverService private configurationResolverService: IConfigurationResolverService + @IConfigurationResolverService private configurationResolverService: IConfigurationResolverService, + @IMessageService private messageService: IMessageService ) { // noop } @@ -541,11 +544,11 @@ class Launch implements ILaunch { return this.configurationResolverService.resolveInteractiveVariables(result, adapter ? adapter.variables : null); } - 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 @@ -566,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++; } } @@ -590,7 +593,13 @@ 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)); }); @@ -605,9 +614,10 @@ class WorkspaceLaunch extends Launch implements ILaunch { @IWorkbenchEditorService editorService: IWorkbenchEditorService, @IConfigurationService configurationService: IConfigurationService, @IConfigurationResolverService configurationResolverService: IConfigurationResolverService, - @IWorkspaceContextService private workspaceContextService: IWorkspaceContextService + @IWorkspaceContextService private workspaceContextService: IWorkspaceContextService, + @IMessageService messageService: IMessageService ) { - super(configurationManager, undefined, fileService, editorService, configurationService, configurationResolverService); + super(configurationManager, undefined, fileService, editorService, configurationService, configurationResolverService, messageService); } get uri(): uri { @@ -622,7 +632,7 @@ class WorkspaceLaunch extends Launch implements ILaunch { return this.configurationService.inspect('launch').workspace; } - openConfigFile(sideBySide: boolean, type?: string): TPromise<{ editor: IEditor; configFileCreated: boolean; }, any> { - return this.editorService.openEditor({ resource: this.workspaceContextService.getWorkspace().configuration }).then(editor => ({ editor, configFileCreated: false })); + 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 288a5760b2f..d737a4165f4 100644 --- a/src/vs/workbench/parts/debug/electron-browser/debugService.ts +++ b/src/vs/workbench/parts/debug/electron-browser/debugService.ts @@ -818,12 +818,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); }) ); } From 0583f92291c52b2378fd6fd485bd506a184cffd5 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Tue, 23 Jan 2018 11:15:35 +0100 Subject: [PATCH 113/128] #36967 Support writing launches in workspace configuration file --- .../node/configurationEditingService.ts | 17 ++++++++++------- .../test/node/configurationService.test.ts | 4 ++-- 2 files changed, 12 insertions(+), 9 deletions(-) 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/test/node/configurationService.test.ts b/src/vs/workbench/services/configuration/test/node/configurationService.test.ts index 1989f5e6baf..424b704b3d5 100644 --- a/src/vs/workbench/services/configuration/test/node/configurationService.test.ts +++ b/src/vs/workbench/services/configuration/test/node/configurationService.test.ts @@ -1138,10 +1138,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', () => { From 1cacde34962c0c03cfa2de5eb72b3dbbc772e992 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Tue, 23 Jan 2018 11:23:02 +0100 Subject: [PATCH 114/128] Revert "add 'configuration' (read uri/path) of workspace config file, #41408" This reverts commit 3ecef8c1dd1d23afdab60ee8a5ff0b7006498810. --- src/vs/platform/workspace/common/workspace.ts | 2 +- src/vs/workbench/api/node/extHost.protocol.ts | 1 - src/vs/workbench/api/node/extHostExtensionService.ts | 2 -- 3 files changed, 1 insertion(+), 4 deletions(-) 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 faacf07efa7..d4dcd03d363 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) ); From c903bd7120cbf3afb4beffc5bce00a3e842c2226 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Tue, 23 Jan 2018 11:58:50 +0100 Subject: [PATCH 115/128] Remove default keybinding for SelectToBracket --- src/vs/editor/contrib/bracketMatching/bracketMatching.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/vs/editor/contrib/bracketMatching/bracketMatching.ts b/src/vs/editor/contrib/bracketMatching/bracketMatching.ts index 64258d6d816..98de48b6899 100644 --- a/src/vs/editor/contrib/bracketMatching/bracketMatching.ts +++ b/src/vs/editor/contrib/bracketMatching/bracketMatching.ts @@ -51,11 +51,7 @@ class SelectToBracketAction extends EditorAction { id: 'editor.action.selectToBracket', label: nls.localize('smartSelect.selectToBracket', "Select to Bracket"), alias: 'Select to Bracket', - precondition: null, - kbOpts: { - kbExpr: EditorContextKeys.textFocus, - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_Q - } + precondition: null }); } From 653182731a306dd0aef65c0f71adcb77a5c6b8a7 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 23 Jan 2018 12:04:15 +0100 Subject: [PATCH 116/128] fix #42031 --- src/vs/workbench/browser/parts/editor/editorActions.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) 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(); } From 24ecf49479920cb624286d27a0b5ba26a80e0b22 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Tue, 23 Jan 2018 12:11:16 +0100 Subject: [PATCH 117/128] make =~ actually be a regex test operation, #26044 --- .../platform/contextkey/common/contextkey.ts | 41 +++++++++++-------- .../contextkey/test/common/contextkey.test.ts | 13 +++--- 2 files changed, 30 insertions(+), 24 deletions(-) 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); From 36cafba0ac1693e5f58cd73842644311a9d6434e Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 23 Jan 2018 12:20:31 +0100 Subject: [PATCH 118/128] ext dev - delete untitled workspace on close --- src/vs/code/electron-main/windows.ts | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) 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) { From 2140e0320e30d50bf30ee5ce76a33b0f224bac50 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Tue, 23 Jan 2018 12:25:02 +0100 Subject: [PATCH 119/128] Paint secondary cursors with the same opacity as the primary cursor --- .../editor/browser/viewParts/viewCursors/viewCursor.ts | 10 ++-------- .../browser/viewParts/viewCursors/viewCursors.css | 3 --- .../browser/viewParts/viewCursors/viewCursors.ts | 4 ++-- 3 files changed, 4 insertions(+), 13 deletions(-) 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); } From 35aa8fb5316612ae713ffa40375332b7ae6ac6e2 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Tue, 23 Jan 2018 12:28:04 +0100 Subject: [PATCH 120/128] Fix languageId typo --- .../sharedProcess/contrib/languagePackExtensions.ts | 6 +++--- .../extensionManagement/common/extensionManagement.ts | 2 +- .../test/common/extensionEnablementService.test.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) 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/platform/extensionManagement/common/extensionManagement.ts b/src/vs/platform/extensionManagement/common/extensionManagement.ts index 9789db5e966..8ae29d3f01c 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; } 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); }); }); From 6c16a67536d395b127021eb9ccc78266f0c3ed10 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Tue, 23 Jan 2018 12:33:31 +0100 Subject: [PATCH 121/128] Show localizations contributions in the extensions editor --- .../extensions/browser/extensionEditor.ts | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) 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 || []; From b9cdcecf333f2b924ac5ffe9269f7d6902ad5ef8 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 23 Jan 2018 12:35:32 +0100 Subject: [PATCH 122/128] polish from feedback --- src/vs/code/electron-main/window.ts | 2 +- .../actions/browser/menuItemActionItem.ts | 33 +++++++------------ src/vs/platform/actions/common/actions.ts | 2 +- .../electron-browser/menusExtensionPoint.ts | 4 +-- .../parts/editor/editor.contribution.ts | 4 +-- .../electron-browser/toggleWordWrap.ts | 4 +-- .../electron-browser/debug.contribution.ts | 2 +- .../fileActions.contribution.ts | 2 +- 8 files changed, 22 insertions(+), 31 deletions(-) diff --git a/src/vs/code/electron-main/window.ts b/src/vs/code/electron-main/window.ts index bbe6e486f12..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(typeof item.iconPath === 'string' ? item.iconPath : item.iconPath.dark); + icon = nativeImage.createFromPath(item.iconPath.dark); if (icon.isEmpty()) { icon = void 0; } diff --git a/src/vs/platform/actions/browser/menuItemActionItem.ts b/src/vs/platform/actions/browser/menuItemActionItem.ts index 787ae98fb14..93dad79a5e7 100644 --- a/src/vs/platform/actions/browser/menuItemActionItem.ts +++ b/src/vs/platform/actions/browser/menuItemActionItem.ts @@ -127,12 +127,12 @@ export class MenuItemActionItem extends ActionItem { private _itemClassDispose: IDisposable; constructor( - private action: MenuItemAction, + public _action: MenuItemAction, @IKeybindingService private _keybindingService: IKeybindingService, @IMessageService protected _messageService: IMessageService, @IContextMenuService private _contextMenuService: IContextMenuService ) { - super(undefined, action, { icon: !!(action.class || action.item.iconPath), label: !action.class && !action.item.iconPath }); + super(undefined, _action, { icon: !!(_action.class || _action.item.iconPath), label: !_action.class && !_action.item.iconPath }); } protected get _commandAction(): IAction { @@ -150,7 +150,7 @@ export class MenuItemActionItem extends ActionItem { render(container: HTMLElement): void { super.render(container); - this._updateItemClass(this.action.item); + this._updateItemClass(this._action.item); let mouseOver = false; let altDown = false; @@ -200,9 +200,9 @@ export class MenuItemActionItem extends ActionItem { _updateClass(): void { if (this.options.icon) { if (this._commandAction !== this._action) { - this._updateItemClass(this.action.alt.item); + this._updateItemClass(this._action.alt.item); } else if ((this._action).alt) { - this._updateItemClass(this.action.item); + this._updateItemClass(this._action.item); } } } @@ -213,23 +213,14 @@ export class MenuItemActionItem extends ActionItem { if (item.iconPath) { let iconClass: string; - if (typeof item.iconPath === 'string') { - if (MenuItemActionItem.ICON_PATH_TO_CSS_RULES.has(item.iconPath)) { - iconClass = MenuItemActionItem.ICON_PATH_TO_CSS_RULES.get(item.iconPath); - } else { - iconClass = ids.nextId(); - createCSSRule(`.icon.${iconClass}`, `background-image: url("${URI.file(item.iconPath).toString()}")`); - MenuItemActionItem.ICON_PATH_TO_CSS_RULES.set(item.iconPath, iconClass); - } + + if (MenuItemActionItem.ICON_PATH_TO_CSS_RULES.has(item.iconPath.dark)) { + iconClass = MenuItemActionItem.ICON_PATH_TO_CSS_RULES.get(item.iconPath.dark); } else { - 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).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); - } + 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); diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index 40703ce8563..f8786606a24 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -23,7 +23,7 @@ export interface ICommandAction { id: string; title: string | ILocalizedString; category?: string | ILocalizedString; - iconPath?: string | { light: string; dark: string; }; + iconPath?: { dark: string; light?: string; }; precondition?: ContextKeyExpr; } diff --git a/src/vs/platform/actions/electron-browser/menusExtensionPoint.ts b/src/vs/platform/actions/electron-browser/menusExtensionPoint.ts index be1f3f20705..3955afd293e 100644 --- a/src/vs/platform/actions/electron-browser/menusExtensionPoint.ts +++ b/src/vs/platform/actions/electron-browser/menusExtensionPoint.ts @@ -286,10 +286,10 @@ ExtensionsRegistry.registerExtensionPoint { 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/files/electron-browser/fileActions.contribution.ts b/src/vs/workbench/parts/files/electron-browser/fileActions.contribution.ts index a1c5b1628ef..f365329c68a 100644 --- a/src/vs/workbench/parts/files/electron-browser/fileActions.contribution.ts +++ b/src/vs/workbench/parts/files/electron-browser/fileActions.contribution.ts @@ -125,7 +125,7 @@ appendSaveConflictEditorTitleAction('workbench.files.action.revertLocalChanges', dark: URI.parse(require.toUrl(`vs/workbench/parts/files/electron-browser/media/undo-inverse.svg`)).fsPath }, -9, revertLocalChangesCommand); -function appendSaveConflictEditorTitleAction(id: string, title: string, iconPath: { dark: string; light: 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); From 9df63af4eec827c00a24d4a0bbc7433b03e225da Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 23 Jan 2018 12:59:00 +0100 Subject: [PATCH 123/128] fix leak --- src/vs/platform/actions/browser/menuItemActionItem.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/vs/platform/actions/browser/menuItemActionItem.ts b/src/vs/platform/actions/browser/menuItemActionItem.ts index 93dad79a5e7..2ce14fddef7 100644 --- a/src/vs/platform/actions/browser/menuItemActionItem.ts +++ b/src/vs/platform/actions/browser/menuItemActionItem.ts @@ -227,4 +227,13 @@ export class MenuItemActionItem extends ActionItem { this._itemClassDispose = { dispose: () => this.$e.getHTMLElement().classList.remove('icon', iconClass) }; } } + + dispose(): void { + if (this._itemClassDispose) { + dispose(this._itemClassDispose); + this._itemClassDispose = undefined; + } + + super.dispose(); + } } From 3ecae53188f00f569ef31e08a4cbf51cec7b4619 Mon Sep 17 00:00:00 2001 From: isidor Date: Tue, 23 Jan 2018 16:13:16 +0100 Subject: [PATCH 124/128] fixes around mr compound debugging --- src/vs/workbench/parts/debug/browser/debugActions.ts | 2 +- src/vs/workbench/parts/debug/common/debugModel.ts | 2 +- .../parts/debug/electron-browser/debugCommands.ts | 4 ++-- .../parts/debug/electron-browser/debugService.ts | 9 +++++---- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/parts/debug/browser/debugActions.ts b/src/vs/workbench/parts/debug/browser/debugActions.ts index 562bae5c743..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.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/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/debugCommands.ts b/src/vs/workbench/parts/debug/electron-browser/debugCommands.ts index c0610e8e9a9..3387c35f8a5 100644 --- a/src/vs/workbench/parts/debug/electron-browser/debugCommands.ts +++ b/src/vs/workbench/parts/debug/electron-browser/debugCommands.ts @@ -163,13 +163,13 @@ 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.uri.toString() === workspaceUri).pop() || manager.selectedLaunch; + const launch = manager.getLaunches().filter(l => l.uri.toString() === launchUri).pop() || manager.selectedLaunch; return launch.openConfigFile(false).done(editor => { if (editor) { diff --git a/src/vs/workbench/parts/debug/electron-browser/debugService.ts b/src/vs/workbench/parts/debug/electron-browser/debugService.ts index d737a4165f4..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,7 +669,7 @@ export class DebugService implements debug.IDebugService { this.model.getBreakpoints().forEach(bp => bp.verified = false); } this.launchJsonChanged = false; - const launch = root ? this.configurationManager.getLaunches().filter(l => l.uri.toString() === root.uri.toString()).pop() + 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; @@ -692,7 +693,7 @@ 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 => { + return sequence(compound.configurations.map(name => () => { if (name === compound.name) { return TPromise.as(null); } @@ -738,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.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); @@ -1035,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] From f3ffbcf5408eb972ec61b0b45ade88ff223df154 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Tue, 23 Jan 2018 15:48:20 +0100 Subject: [PATCH 125/128] fix bad state change in darwin update service --- .../platform/update/electron-main/updateService.darwin.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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)); From 53a2781d80213421a679d43fe833c9355da0bda0 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Tue, 23 Jan 2018 15:49:41 +0100 Subject: [PATCH 126/128] add extensions control url --- package.json | 4 +- .../common/extensionManagement.ts | 7 +++ .../node/extensionGalleryService.ts | 44 ++++++++++++++++++- src/vs/platform/node/product.ts | 1 + 4 files changed, 53 insertions(+), 3 deletions(-) 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/platform/extensionManagement/common/extensionManagement.ts b/src/vs/platform/extensionManagement/common/extensionManagement.ts index 8ae29d3f01c..6512e977822 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -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/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; }; }; From 5bdb90de36863ae2fdfbc424e34800f4e4eb484a Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Tue, 23 Jan 2018 16:40:41 +0100 Subject: [PATCH 127/128] scripts/code.sh picks up marketplace built-in extensions automatically --- build/builtInExtensions.json | 4 +++ build/gulpfile.vscode.js | 26 ++++++++++++--- build/lib/builtInExtensions.js | 32 ++++++++++++++++++ extensions/ms-vscode.node-debug/package.json | 4 +-- extensions/ms-vscode.node-debug2/package.json | 4 +-- scripts/code.sh | 3 ++ .../electron-browser/extensionPoints.ts | 4 +++ .../electron-browser/extensionService.ts | 33 ++++++++++++++++++- 8 files changed, 100 insertions(+), 10 deletions(-) create mode 100644 build/builtInExtensions.json create mode 100644 build/lib/builtInExtensions.js diff --git a/build/builtInExtensions.json b/build/builtInExtensions.json new file mode 100644 index 00000000000..4921d2a27c7 --- /dev/null +++ b/build/builtInExtensions.json @@ -0,0 +1,4 @@ +[ + { "name": "ms-vscode.node-debug", "version": "1.20.3" }, + { "name": "ms-vscode.node-debug2", "version": "1.20.1" } +] \ No newline at end of file diff --git a/build/gulpfile.vscode.js b/build/gulpfile.vscode.js index 15b95986668..b0ff0a2a732 100644 --- a/build/gulpfile.vscode.js +++ b/build/gulpfile.vscode.js @@ -44,14 +44,13 @@ const nodeModules = ['electron', 'original-fs'] // Build -const builtInExtensions = [ - { name: 'ms-vscode.node-debug', version: '1.20.3' }, - { name: 'ms-vscode.node-debug2', version: '1.20.1' } -]; +const builtInExtensions = require('./builtInExtensions'); const excludedExtensions = [ 'vscode-api-tests', - 'vscode-colorize-tests' + 'vscode-colorize-tests', + 'ms-vscode.node-debug', + 'ms-vscode.node-debug2', ]; const vscodeEntryPoints = _.flatten([ @@ -584,3 +583,20 @@ gulp.task('generate-vscode-configuration', () => { console.error(e.toString()); }); }); + +//#region Built-In Extensions +gulp.task('clean-builtInExtensions', util.rimraf('.build/builtInExtensions')); + +gulp.task('builtInExtensions', ['clean-builtInExtensions'], function() { + const marketplaceExtensions = es.merge(...builtInExtensions.map(extension => { + return ext.fromMarketplace(extension.name, extension.version) + .pipe(rename(p => p.dirname = `${extension.name}/${p.dirname}`)); + })); + + return ( + marketplaceExtensions + .pipe(util.setExecutableBit(['**/*.sh'])) + .pipe(vfs.dest('.build/builtInExtensions')) + ); +}); +//#endregion diff --git a/build/lib/builtInExtensions.js b/build/lib/builtInExtensions.js new file mode 100644 index 00000000000..0a78f4ccffd --- /dev/null +++ b/build/lib/builtInExtensions.js @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const root = path.dirname(path.dirname(__dirname)); + +function isUpToDate(extension) { + const packagePath = path.join(root, '.build', 'builtInExtensions', extension.name, 'package.json'); + if (!fs.existsSync(packagePath)) { + return false; + } + const packageContents = fs.readFileSync(packagePath); + try { + const diskVersion = JSON.parse(packageContents).version; + return (diskVersion === extension.version); + } catch(err) { + return false; + } +} + +const builtInExtensions = require('../builtInExtensions'); +builtInExtensions.forEach((extension) => { + if (!isUpToDate(extension)) { + process.exit(1); + } +}); +process.exit(0); diff --git a/extensions/ms-vscode.node-debug/package.json b/extensions/ms-vscode.node-debug/package.json index 87569809e43..9d065dd4a5c 100644 --- a/extensions/ms-vscode.node-debug/package.json +++ b/extensions/ms-vscode.node-debug/package.json @@ -1,7 +1,7 @@ { - "name": "node-debug-placeholder", + "name": "node-debug", "version": "1.6.0", - "publisher": "vscode", + "publisher": "ms-vscode", "engines": { "vscode": "1.6.x" } diff --git a/extensions/ms-vscode.node-debug2/package.json b/extensions/ms-vscode.node-debug2/package.json index 26777f7dae6..96c454263b3 100644 --- a/extensions/ms-vscode.node-debug2/package.json +++ b/extensions/ms-vscode.node-debug2/package.json @@ -1,7 +1,7 @@ { - "name": "node-debug2-placeholder", + "name": "node-debug2", "version": "0.0.3", - "publisher": "vscode", + "publisher": "ms-vscode", "engines": { "vscode": "1.6.x" } diff --git a/scripts/code.sh b/scripts/code.sh index 6339ad06bd4..089d380c1b5 100755 --- a/scripts/code.sh +++ b/scripts/code.sh @@ -24,6 +24,9 @@ function code() { # Get electron node build/lib/electron.js || ./node_modules/.bin/gulp electron + # Get built-in extensions + node build/lib/builtInExtensions.js || ./node_modules/.bin/gulp builtInExtensions + # Build test -d out || ./node_modules/.bin/gulp compile diff --git a/src/vs/workbench/services/extensions/electron-browser/extensionPoints.ts b/src/vs/workbench/services/extensions/electron-browser/extensionPoints.ts index b0b0f300177..1ce6fdca2c2 100644 --- a/src/vs/workbench/services/extensions/electron-browser/extensionPoints.ts +++ b/src/vs/workbench/services/extensions/electron-browser/extensionPoints.ts @@ -334,6 +334,10 @@ export class ExtensionScanner { } const rawFolders = await pfs.readDirsInDir(absoluteFolderPath); + + // Ensure the same extension order + rawFolders.sort(); + let folders: string[] = null; if (isBuiltin) { folders = rawFolders; diff --git a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts b/src/vs/workbench/services/extensions/electron-browser/extensionService.ts index 8abc3ff6140..4d51f56bd85 100644 --- a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts +++ b/src/vs/workbench/services/extensions/electron-browser/extensionService.ts @@ -44,6 +44,7 @@ import * as strings from 'vs/base/common/strings'; import { RPCProtocol } from 'vs/workbench/services/extensions/node/rpcProtocol'; const SystemExtensionsRoot = path.normalize(path.join(URI.parse(require.toUrl('')).fsPath, '..', 'extensions')); +const ExtraDevSystemExtensionsRoot = path.normalize(path.join(URI.parse(require.toUrl('')).fsPath, '..', '.build', 'builtInExtensions')); // Enable to see detailed message communication between window and extension host const logExtensionHostCommunication = false; @@ -624,6 +625,36 @@ export class ExtensionService extends Disposable implements IExtensionService { log ); + let finalBuiltinExtensions: TPromise = builtinExtensions; + + if (devMode) { + const extraBuiltinExtensions = ExtensionScanner.scanExtensions(new ExtensionScannerInput(version, commit, locale, devMode, ExtraDevSystemExtensionsRoot, true), log); + finalBuiltinExtensions = TPromise.join([builtinExtensions, extraBuiltinExtensions]).then(([builtinExtensions, extraBuiltinExtensions]) => { + let resultMap: { [id: string]: IExtensionDescription; } = Object.create(null); + for (let i = 0, len = builtinExtensions.length; i < len; i++) { + resultMap[builtinExtensions[i].id] = builtinExtensions[i]; + } + // Overwrite with extensions found in extra + for (let i = 0, len = extraBuiltinExtensions.length; i < len; i++) { + resultMap[extraBuiltinExtensions[i].id] = extraBuiltinExtensions[i]; + } + + let resultArr = Object.keys(resultMap).map((id) => resultMap[id]); + resultArr.sort((a, b) => { + const aLastSegment = path.basename(a.extensionFolderPath); + const bLastSegment = path.basename(b.extensionFolderPath); + if (aLastSegment < bLastSegment) { + return -1; + } + if (aLastSegment > bLastSegment) { + return 1; + } + return 0; + }); + return resultArr; + }); + } + const userExtensions = ( environmentService.disableExtensions || !environmentService.extensionsPath ? TPromise.as([]) @@ -646,7 +677,7 @@ export class ExtensionService extends Disposable implements IExtensionService { : TPromise.as([]) ); - return TPromise.join([builtinExtensions, userExtensions, developedExtensions]) + return TPromise.join([finalBuiltinExtensions, userExtensions, developedExtensions]) .then((extensionDescriptions: IExtensionDescription[][]) => { const system = extensionDescriptions[0]; const user = extensionDescriptions[1]; From 018cc146a786cf3adf04ab5d56b6a0f301e5ea3e Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Tue, 23 Jan 2018 16:33:06 +0100 Subject: [PATCH 128/128] use 32 bit version of inno_updater --- build/win32/inno_updater.exe | Bin 181248 -> 147456 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/build/win32/inno_updater.exe b/build/win32/inno_updater.exe index 6b8c7a981423d8c8c8cd66b1389b5864d2731d41..c83bcd48cacd90aaa9e5e0a2fda90ae0f568e210 100644 GIT binary patch literal 147456 zcmeFa3wTu3)$l(P1{h@I3=kn=TB43EQP4zDiJ~Ns1XR#LAVCGis%aXjVw?fmA_iun zIUdH+mcF&E{a3%5KI6s+$?fY1hl%xK@F(fW&Xdl&pEk)Z~MOA_kW+~ z|2$uKn6u8luD#aUYps1flWtz>>EZEseEd(RJ)Y-z%3q=W{m*|C(fx)t^z*Ff^V-?Z zdndkj_SDL6-Jdh3`rdC=-~R2KJ8r+{o_ixX-?%HMI(kpex9-U)zF|tvx9`34t_x2; z{j`u%wE5ERmtNcT=sx%Vn%{n9UlZ>`8@~V12K`JpPwS(1^!pk`$GafZ(Wk%X%irv z;i)9+Nq@64JZt-TJkMT~;hB0;WJiEMkJ>t3HJQI)|T>uJFR@yJp`@LTS5a7V$zm+SvK85cE60|6ln390khDV*C77zEv8mZHx?Q z@pxhjGd+?1*218Arp4qzMHmn;U$;tq)zN|0I1V83 z#7!`>lmp2aNv3p+bHqhDPx_HI!GWu={E^?SK#@>y?-o%1TTPh0Nj*@BMutt2k6l zL~&>|L7BRQfPR;fCrYAVW{96`ekgr9zY{}Y9Y+f4m_P~g4y5De=r}*6<>+|9YGSC8 z-x;AbqK*~AamF(LJfB9Es^{tYNs%V~XV&HS!?ewr4*Y%=G)D5e7;%`zv3GOhW3 z^O(6H6f8Ivy)0!^7(?S)X?mLpIM13L%3E~8ljHFiKWy6azInh*s*Ar!r_Fb~hs+23 z#^O(f_xWo}7+^auUU%U|5|yNR(bMwjkv5MsG4!LNo~2XS6pG z7@c9%*4WtsH3dd=-sp^KlPUvNi9a3nrGui4O8i$(%I??nwTCLlCFCx zVCtNuG|W%`tu00orpnZrgraBD_h}{7pEuQupJ|e#2SC9wBR-U*$uay$UNl3XUd*@^ z31yo7q$Osl&n)wsH~C|0ve5)Ho6T+c>n@;Csh#F_^Qb!VXeTMN=^L;XgoaZ>PHozV z|5Hkv5gJB|Lgw!BDbvDg&>1!{Mu)tSuP2`(flF_qp;sWRKFw@TV$?n)DIl2-lJ#^+ z_G2j*v4UhqZ5=O=t&vLnry$!1E9!HUc&|oBEB63Ei649k8qU@9*$*gKtGxi4M(;=n zcqi|KKE}{OsnACUzoKA+u~;O;`%3NxbGK1@9`UW(*-K?YN1q_p+Diy&qU4}po2^G@ zHrKa(iXuIW81?-;ug7dv5h|$)2ul_Kp|vk>@$`RWVko0-LJ#A)K&Zl;;5Fy_S_}OX zykk9fsja0xaeB;Bf9lupAeyjMnOgAoPRyXc+x*BBewbU#*M%RSlaDqHGk2R!=8p1d zYFn<5+8?vI(Li*?g)TqPj*{eW9Z8JZ$4GG!H-0Cjy4?5~FK}bE83_d;s2`1yp6dv5 zsV&GO(WrDh+q4c;b6$iCk&vcB6>ut3&E!Fe0Y`YJ@~W=kN1N1AiKO)8EfBt<17C+f zql7<;!NU8R&7c1NfSK>ua#{1)!KX*t~4^+H$fy!vXH zCJ&v?+)wj^YR0j2n(l74-D>M+n}1pGYb0GW&)j20{nONCb}DP8-`tklW-R`r5PzCS zxJ(ZTcn^ftB^YT6Lf-_u(QM&u^!#?g?5zd!2ERGpm%2;3J&F{gy~`XA)W3IXa($x+ z={XduJ{$oVn$?C?E*ahgf(#`so&ZAHPYWrPYAK!GE~UDP*73HKj`y2Uzj-*eCd*bJ z`^_C{^&>(HL07dlw3Eec?}E@Mk=3`QVbb;)w0(E^v?*bA?Le&`%!nk%E<=LH#k@k``$gmtT{G2Vb{+CpyvKx3@ok-lGRjF$if zH}x!l7R+Y=k`*G98b5Oz(Z1T9k~eV>XM<7(pr;H@uaxCV#0j<)xiR zB_7SBNq)1$XDXL;^})|zOg72YCU1KMPk>` zT4M94#2k^7|s;H^!}%P}rR%wzi$!8JgSI`pvO;+&Q)`>frTV=WAYH zOLaD{Zv+xv|CEy8b?ry+d63f;GrJlL(V?wgDZaJHW_6K2@^fMJfBb7!Z-Y^q)xt3- zv6hF!>SjRoJU^P%e*;L4rG=W+|9r?{b*B1oWqaqw9*Ms>j7EZcGF zFev{g_{Cu|1EE&Ts1VBBgHEztKs-+1soI}fruAFAG4fU0r^+Rp&zc~rOl&UcdL`FN zqqS9*G@j_~$(4lE50^e<0IAb+HC+!p=hF2kkkIu%h;8u%q3QsKs==;Q?QVv31(BO; z7KHp6)l-n?fHmHqp67_zcvr+8bVckzD$;~q003d{<>3&v3Q!7Ax43Dh1BeGP3}+UN z8B{<1n?qD*Jhj!_phl3W%CCJvpxVZ0A9IhItrH@m+2$y7j<4Yg$@RUm>yGS=DUeZ)9S}ClfkAsN_GYs5O}0Foyt;!kbtVNh{acd2)^Vc z=~i9F$9e?0X|cvl1_6PPw1Pg!&`OWvk)(4pT3?1wVK;mj`8xPib;IYO72uO6_&f^q zpWsvUpX2kxzX-bE6Nc(pB3X{?#Ev{T@ASGr2oKTYNs4J$bR^x|q(;EB2K0s1+20pN=X9OXVtxrPx*k28wt6uW z=4WOt+S}Z(#_hwgl!-3p>U_!zQ?&Lu0Tnx@@IK^C2Y&!ymFDPj+)@IumK<~ct+yrD zQj|NY?pnL!&!#U^>vg%hy170jywXXWhfsCm-8&mt&rf+N^6NBY73sLoxnI1f4z3rO*$8Nbl1fbqp9*(B3 z0kq}@R5edcpL+pZ(&`Fh`$mdgGxv(4jFQY&2no*|kxibV`R6bsEDjBG-hCU7=y<=V zW3=&bda&dR=3P)IlO36qliiTTbRB_No}cQco zppflUnd6`kl6PG+^~&UipTq%O@5InB-m`euQ~KZ(1kNlhLi3&9rP}{b>znh-LXT>Q zc5bF^@1`O>mDYG>nS$f-FoHLTBQh8?na{%Y7_B?rNQn@?0;=jh7(#4AR#~uivk^Z; z+pW^f+Qvj74lK@@hXzjwtEYc1ax25I3CPnXNi{Vz(cHseFRUKyX-6W*(c3UKK5KT! zuYUYzgyQr@Cd17ZW?|3Q99ueYtfwkdjd(ztZHmkG)<3bN$YT_vIn1>W_muQg%F>&# zR{hjD6rn|ZD1h!JWp+2Y0hCWQ8qe(*?Y#ddC-1uei0f7!PB+;Es9N~Zvf2zF6p;St zs9yE<&4lTRL#6>oL9nTzVf@N0(jLeu-W$BvG!#l^hSc{`XsSr4_+L%J-|9}nmr|{6 z+c*I5yNri}Up`=Ksb7?PD_(+rXq)ZM%=eOIfj$tlSoKipv^x5X)8!8V#MVsJZg>7> zvq`;opDiV}y`xFlAem#0vP!e7POp6PjIqoaWu?Obbdrg&f4@#TDJrO?IMmaVcrcK_ zb!d2xWaj?LeLSXA^;Ao>EHKiSgbl|Dt@I7DqoQ^cOzP>uJ@G~Ip7qApMMIV^)UfNq zv7Q%q^JDoBHsRrB8zl#-GRo3(veJ`v{Y|>^wT^mpd#?SobxyFc$~?lPWR(nmrn%&DWls4ypvPA48bKJRa-4D-|2u{#%>TQ60Z+Yarq3&?FY zo{OI)8O$veX7T8Btn~!;uP}D3&x3txn)B+iN8gYNx@p`LFADaF;?Z7)lpu7X440;| z;oPwvm0K;m8>M-t#}_iF%7R_lHoJOO4(r#MU3*}dP)xH*Sb>7|H;f=nGhqUQpwV0w;EUHDxo*VoBt<~MeFfn_$oGEoG>Q4}MgOP&=CSTlnKyP`b{ zrupX<7wo!!p!I-1QQ~9r_Mv;AMiP7GA;>Bb^{nL{5f4^*CYOiPQIERzkJ`@~2`;Su zn0r(6obGDw#tpl1vZVQ%ohFxxhaq$%`!M+`gjUD?0Lq8I9qE0QEZ;|mO9ApQfObjd+dP^&ZJ!A#Li zU-Au7);5|mgORy1RK4#MQ;0WooM7&u*J;`U83@$DV4`1LXzZ7Clp6_gRrAe{+>by~;Zh z(mpQJ%Y9fUQ9oJe(4*?+1KRTuqB*kFd7e4dH#Wu->1Pjmok{2BdOh~o-^I{6YQmPc zATi!h*d0S;bH+`YM{E82jdQBviF<3sTus}S(uw7jvi5Pn}6=`#0@@X z4_CYkKH~$4K$dw!pvQQhtSWrWcmm2hZNJr*x~^q>&};RUu_hfuPbNrn=onL`rSsGf ziY2bE&=KsB>P!_Nj7fDSGsK4IEM_l?(Zg@?+L^%D)clq0M%eTVSyx>7U0LMBep|fZRxLv?NxULvv%-18|Q`h~@A;?q!2Ke`+ZfzArJ;^h9AgsZI&0Z_R zy3rRMQ1H3&&|pHT;U(kI(}BGE&8^-OMx6m{HiHe)AGy{1hx+Wdj@W+zV17u!lUxO{ zM_uq65l~GJ#P36sY2w-14fIRJYSnF^i##L(!zj<+!xx%X7FKuaF<&4x)6Vx#$rb6y zWo~8kdteV7)H(B6y-8U9#%c=M3Nil0#zb3$0yd_g$xD7 zk;4Ws{3gg7TNAPw_k1Si;=}u;LFR|k%G95~BLd^6|LFnnMjg}5ewzOj7vR0ai}r1# zz1GE7R{Myyg7b;tYA_^?n`=G)M z>(+o;1XZi%%OKbU!p}Qs#QXN8kR$X&re4e)q?~}zPCSh=d4_p-jCc=8BSYPINIIwd z@j5;EIXrDC3VM={k={IDRR<`s?l-(x86frq>8$U9_$PhfnU5;9nmZjp77KR~nv9dU zVrgP5xc4v~`Y{z*MP5Ykj;EMlJtFnC9(rffC*}z+C0|G4+O%*bG7SU@%`NIrzm`({ zVf7pUvN*lAv0n5&(ZxpbCe0&4jpEnImTAPtp;KX1p}8Y9Of@{Kbp~o7%REr7M%=E|FaC8F)c&cR-j$|ez4TnHm!2!N zgw3LHIg}W-CGSf36}gpynVBg}guNn(P^tJ)zq!$tNYeQp1v_CL_=h9?)!bicvsEm@ zFb7x!0P+<=Fh3+&JOxsFx99GNW+r>WCor9$=6 zFIflz1*^^1jp74|>%9dnu)gM4TXld63Yv_P^|GEy5lf`{L?krJTxE0dD86Sr^c0AN z)$DD0z+>}A-O99|@P|JpjjCd;nwVP$kYQ!Iv7B`tK zuq)u*WUlXMkFoNriG_B1PZVrDMSEME_P%KpA0k0}d+qngC8Y&#(%v!B-f7fW@TR%n zDBh5`&s(s`DBf?km&R{3O7`paZq@A#m`#G?xppIbj-XnrG;iqKBCle60GIKQKR{V{ zl|QKSEDy_;qlB+k0u#Q%M4&fOvL%x)=BxkXAO3Lup(I^IN;5ZZI@+X$dRMF5x3%#2 ztjBePwL+Im7m~h0YTc6{GQ`S;d?Cup5z6AhWvKx3WN=`X$9z1rg!GYX627@I*_+6Y z>Gg5lNl?DCB#U{plv3>8p*MG!<3wW@>-`-)V$A`y>abH@^ku|g2GS{wgb>nfTrFF7 zsmV`xJ(WTc=_O^n2=z9jHO&1OGHOF#+$r(M&zK-6NPzB&oM#ybY9iY!BJ@aqroRL6&GRp!Tl$QZ2a-Z1W2jV= zDHZYbi@wU=nfx}-_WdJuTWj7QDlTrX{6=$os-Lm4$V(p*{={~VF;*U|Ir`4rGYYn& z?){Mg#>)I69rga0RG+5zd!>fuA2hc&?e5j5t>$Q3^}s8|m`KV&#>)OhvAttb1Ld{p zeVKXorTWWn{^r>Gy~)z_{(wGP`?MI(wG_1BQj;wIU@B)(i|pGq+ZMf_h9XVsv_-Fb z-tu@(_t4Lvf*&S`3*$@7?*ScVL|;+_xo)fA2~`UYMFyA~jFscPvDOTUhM+0o9-f?e$RX{SSE5cM zS;k6Zyu{G7%-Efw%rU9{Lf)%H*}5!*W_{tNO=!slI|l1qLg?NQIdytB;zBLI@!abL z`=gLOb*@9){SI-H4sjQ)6Y5r&2P)XN2W_RnM(qLAw1^hEeujC7wf9$Y*Nd8K`Y>ax zI%6AsR3e3z+s#kS6Uo)2hdwg2WeQL0OKO(}=@GHHKQa0&^$W>|>^Hy^Pl9bDY?mVW ztQMd5gr0W@9C}C1cM>Hg_!2Xw3r#AiYnC8mj?OUG!k8Ha&2wc^OeQPDMjut<>`b#5 zB~-@J8Hr5M55DR#W=rCRbYjMp>B&`|8`V4pPG%d(&BlKvP3qik{5MxuXQi?`rY(5B)LO{Y@7@$d#hgnqHsfVW9TdQ&E=qxkDog-T)@ zYCo0xxFelk1Az6#M(z2>(`nH~no4THn>K;0Wm*?NB5Xtlku>qzO?w!PAxchL_X&|v zO?xvEb9{->H+JAJv({PGt;f}$nZeQ1QZnP`hZ7kzr#8ReKGsn+d8&7-<=Ccn= zu8H>iHMwUIai?da&cOZ)Fk1f{nyT;UFO1se|4O;<9r+jKR_k)>O=&uglt=Yn*sbZ| z)4DbN`(L%2PMf6p4Pu$3>C)=@8Y(PP!eb;tv;27K&hPi90~X; zc`N-AVAMY=0YfEV8IY=DT&ByY7q^|k_@N3Zh!X!aLx4owb|q}LTlyYE%X-ybxX8xr zXexJJH4p;3#CF7V#0CJ@2z6PX_PBXs_usSjK=^ zWX+K_Cs(mQ)16NXVpc-1+lST|uK!;aRzKJ&&2V;PUW1zPqhm}r9KfK^VWd5kVRs9 zYQe=flh(+b?Qmxt&K~u{vZ%>yuw+Uu=I1JBO|!}Tph3v;g2Xf>d)4e2TC=V(b(7h2 zm9wOo)JvKTLU?;gbB|H8Zg*WkmNb)kNmH8(dr5Oo@0xXO#FWn~5oiCdm&e;7M!|BT=tKuClG5y}?br3TEF@u_zcW7eHJT);#2{A>d%MSa& z>RbPz2SZHJgw-9usVvBuuWkf{BtB<$=o((t)%<9BUI}m#J?$yBS6}VEzH0SBXLa3c zA}g#~!*^LXPFFFiDLt6^A8MS}q?J92&-4I(Z5q2?Qx_f3h zoC<|?R>UZa4ZO3+ruX27IL2hR8RAx zE&J~QoY6{RMQ9j}DN{2Uv|v*!jC*_L94KOdaPL?0?B|F=`*~@YC*(Iid9TC2Al+L2 zZ>%s*v{V>7U#T#vH+w6Lxvy3j@4QCR^#pF={S6{E5%JDH2;5|+lteo9|7^B#jRmcY6D5mLEBoBYAPQ8RXMgKhk$I1KZJ^*a^A)4? zZF85m#aMh5$*x?XWnZ+9@$lu6kKK10ylmUMf|o^UTa+ZCM?5r=Vx$iwicbqp5t$0A z2N?iSRgQQ{lwr1+-}UEi&wZP^jM_DLozx$&mgq`a1qqKi!Iu;_&y~I8&v~hT78_^p z=zNe(fb0jq^9Sz6rP)9jSOrx@Fw;#we?K8)szy;Ki;-}VtCgBWZz+|JOy zJ;Rw!hRD4wy#kLEhkDeNvdO$SJq5mmOD(=2D_pn0F)Eb=W{BZ|cJ z;SQ*@cHVHMMX|8QG}vc)7@JQS22&W+0i@1qB_Eb{2oJS|5a)=%>wv16AFX&B0jwL? zcjB`y7i7T?-ue!0L{z{MT!Z1Vge}?Ot_ZXJtqZb(k*K0yzyefJc~^WjvKP9@o}r4U zSR;EiKX$#$q^LcJJ_;b9@&_1nz{sWMJ6dxw zM5ZCmd7&=)DZWC4^P1>~4B$qG%#ROkO!>@@^BXP0D)*tXq$pddg#5_#Wa`ffr*F)R zA=rpL%C8b}3bOQ-sbymk82nqyYv&}|U5Kv$vb+}LVYNG?e%{m z%SDe&PKP@oaepTyqJvv|pbGiK*+&2{pAYbG z#C1NP`ZhmWT&nqNBeqt7UTaswiB3vkP+7=fcN}^$S3=} z|E|^0{@j-fJ~2u^F+Udl-%B*S(+zq86Z*Qb_y7&NG7`!&9%e6~rw99d62(0N)7Z85 zBgUaI^!zx#S6!vc8|OD_WsMth1sDR2WIvkCGb28qU`;aYP-ni~!Gch_+J~nVNNWbi z7^bpAR8E6`qmr8Y?IE_U3P{IYQWK z?-rhs3JJ?-58EMO*GO28{GB>oGm1$kjs7ZCK&$d{n>ww(pc94~D>KG26wGg|VdEI5 zr?2rg3lx1f)yLeD>iI&UkQVOATG=${CUdt%JR|3+KBhBEH2{ExUfGhT8H5=7)R>2b z@y`ouDDkhYe-IQ>jY@;8XB<2hdJgJ+A6EoKI?v; z)uUi5M&fg0-1^+t5~KZcDs)s>jaf#AX7&QIv0{0p$>CG;Ze~;=>ge1SqwX8RI@X0v zA$(Y4NZ-0iFl>x2fN^@^0b&Rf_2gAjrmQ-h5W46vy{T1YlEv;#**dckKTeVMM))31 zZ^HfSe8>#yf#*dXr`JR7xvja{l^rCl{Wum?qxLG%SnvRpyvLM3`tbWB4Nl_ zxik4BZFn}rmD{O^@+M9wd_?Pm$?e3M|1fv3vD{ts`6AWN{Fr?L-dIb9`MF;7*~}8^ z+`$E3n3oSI7Lb+=b6M8fyGh&SlAYL}c zZ5RG0d_58iJQ)bvIZloNx_;uJ!+14BWKkd$%tNNJok3~N2#K2>kIjtSy_sFH-DX;C~JB@XnxA7 zOP?*NEPFm(Vi)GGb0cD}c#Y>?5hGpuH4tk`1z9x6ikk30bb$=_+!l(bH^vydmX@*x zblA&Ap2XDtUM)zKFKVTjnY=*^FAHrViv_PoMj0yyC2o}kEEN0!(WA+UbkI&IP~}dF zLN~>1Qsl48KbS0}Fxt30BmW~70cm~mMnd#@V!WnvhS;ivC_(w8| zpgP^7X-}`dfsnnVCbiN`gcoc?d@q$HwJZOV)KfdBzD82tU@yS!aaK29*NbmP{B+jF zoW-{T&f?pL?&8~ed+}{Qi;l?;m=ivA2}xr9of4W!hj55E+G``3?VF4^oF%)%^ioFc z4Wds(I$S-i_C6>|eYdfLmSGOcY|Q z4x({2lpIaC7|3bveyZo^TLTZ_)056m8{UPBO()Q7}sU-0HQZ38)qp@opWvIb<8(Y>H&Iu%5Pc6quW z8%vn@YcfLP`H&$F=+3i-6zG~=SxgKoS5vFQoClt>oOktHfa*m zKTEK&zIy`Wp*gyMKw^4ZVsbEc?Cx#_tOzygdcGyeXiH*xJTZB}Daxso1UD^+HP*zE zHMB-p3AqGycP=I#+DKVp^>v0n$Obbk?yT{7oo9{pZ1FlwVEcH)o-4k08}l(eWuk_e z7MU8}B6BiK3+w4&=8Jc}#rt+5WHR}kx0#dKVW*TtI`#i-$}P;vEQ_zD%^lM+`ItL< zS_aw(HOf_hQ48eI(=Pez5dTIw#mgVy8VA%4K|4K+mCFLsIg9dE>q+Cym&BKefG;|- zie3}l&E#=2w3o~b2``2*3qs-8iE|@giJf4R#!&Pvy1IBo+o2*)bXf2( zVP{pHTPXmFa-yncfB}9ehT4B#L?x@VAf5?hjl{-$M9c2uROqNUk@37^MG_&ssnU<$ zmofiEc0p7#x?bFT&SSI~HBVB6?CbnJW#h-3sjm2*Gj16Npr?UPOD^PSygs5RnGt?z zJsaucq1gb?X$}whGvY;fJo%eZDMI)Bbn09>zob?Vzl@QKYnaJh$$)AYm1I0DhzjZq z%|?nVSwVp!zJzQyS65}AyhV)5RU9|5izfZiaGW^`b@Y~IU zR&$G7B{7U}ng6(!Ck{&=s?@u51^~e4(~{>B@jOBy8d%HW{#rkd}{D$G&$QegR=j7Dq9nZ86T@Ewx3AboHidYkH|!yiapb< z$<;?XH+j|tCpCEzL2fBW)UT-8-ba2Z;T%bImK40lo?-ydr_vNBIL3+2Jn(Hi+2a#|=tJ)#ArVfHyheHx}Xq{6^F>2pL-8r)8_$TxRJsU&+ z2tkq@oaGsw$RJ#T>B=O(mi>nSqyQqb8GOC~{(9yz2L&rS*s0jTGH{#?M>cxsY~5xs z)%%pNjczkmg~TByop`{GNDFNU5yHu7TP>7Y$B!G}_0+7=b!8v~VPmn}RZ6qPOq{$9 zZp2?@vkymEWTS@h@Sk~6tC-*`OFj*l2*_nzT+~3@hlx4hStub1pP86w4vZc7diBKm zZ(^Fw=0izs(aeoaM|v6+v=9r|YSiRlykzd6br|K(pnN@da2Eh3jIxb*B{5vG zg2LlHGqKF3md}eH<ix?|7SZip z8^D$hR}$FKYzr!+T%x5=(^Tk;7>ER$T{ zL5>JIh)5@pKj@Le;&hcLRy}%Q9L14LjzXj25C$VYog&nugtZT!exws{RngZpcO<_! z!ggs=72QDIs8M(H91XhqgJuY!U6t*iwFEz^(iap=$Sq_Fs}B}xhYWEUmhi2tJ!tRs z_M2;sxM;n^oJ=UM<;M0xIR8+3yg~+ctb=;2>-W)#gVyzJJh-j$Gc<$jj?S{KKg=wISJ)gFLsaaCl!JuY9-5)xOE@rO9tK7v6K`9MgU_inGSF0Kf*?4g!~R6 zIO~Je*JnSq6f2?umTAG!;w(e$=Vh7wT(_@a=oIWgyh&yQOEiaU@pnrQn;APy=jiA- z48cfm)b8UKyt%K+h%ZFNsabOzMfoiN6y-4Ssbyz}Lz(-S6P@*t!IM%$J=7 zd#RSbtrg=6tx)u;-tUoJu5Mi1EYYRiFSxT;@|`=gjE5IUqpW*TXvyyY);A)o4LP)0 zJ+fQcqwXcnp6;FpBob(zE0grWNAT1jww|Y_ivV_A_;Sq+&m2k?S3TrdmKjjCmIE8f z|KLZRPPxu{s~^x&_39u;h_y-j6ab64f-=~1OtdIc^`;yB1_1qNBr8y(k?B!fahe!y zCg%8Mz|}t9()*QVKlCq2KE;|5nktljOp0#FjJ58I?t&8wgcA!1gA=19e!j%7(vuK^ zSDj$cU(K*n6ibRSJH@Xxa+#_~qB?dukFsN*aAME3V{3Soyh9{bZtMtJm#N;o=;rz) z^m{(k(+E1Nb&2}u+hWeLhX7MC&!M`Jw`ed?oegJZg>ng z<{R!ZLJfa2ca*u@Ow40PN^&30}VjLG2oSOX}G0##hBK9}ln1+(tPrkU! zh}TK00IpuR$TAh0iaySy3lX5UqO8lcFwI>uzPro~|Z$BBWe(ts)HaM1Rk z996azX>^owSe5DlkSy&@6n?FN&_-SFv*FrV>bFwXjL<~wYyOcJ?Q5>e0YySJS>1;3 zKqpFN%$#+jue$R*jghoi&iL9k7)>i1sw9hdt*GQFv~NOKeLi2PCs)VSEtYz0PYKfd zarp;Eu;U^3rbXf9u3NLlv;zHTGyS8Z6TV)UlC=Cr5*C2*{sUc>$e?QiM;> zM6{Nx{&wrIlQN7a_ZvJKy}ES48LK7{RfUL0kpBU7*Iu!s0jGaqnJ)%QM+}|Rl{M5Q z0+ppM@1FXiQ>QK?^+?sHd+On&hI&s^9-TvQdqLVkW7kR$jU|X31FQ)jGRAX(Y$8*} zhk?l;^eQRXZ*uNj#12P8a3&#+CRu2&6lOehFo$B8-HD}pm?wTLrOjM|qFf<$Vca zpGtYi4R!w;xLr+IBBV}BWFAg8)5UHdrK$2t?BZ?$$h=Rx4uiMyYlO+7e6 zpBkktf@`zJwS2PrC*uXXV5oPuTkPWw^|XL`C`@aGFTN>~3V-k#kta&z=4d`;@YB_37`W$I9}DbQ*f>Q>aTgr298&7>+Stv_oC@ zXVSSdL-D_nNU}(OBTF_tTK)8|Qj!=ezn+)Jv&g8hO`~`R$L~UK9VGB008tnc_yI5( zhKU>U4?BFggl`adg;rs9nv?vc-;qMw)L+|0j1;q%$!Sttomh)s{3C8)JFXriS_|L; zNr!LLyf3=f^^Lwk7^rC9Xokd}=RCGZ#%Yz zS833RJ1O1ujZ(a5+!X-bGOV|z^;cVX1)V&}@EWjtkV`d4I_$7>E@L^b>bLx8+y5&` zX(b+rwR9-hk_mhIqk)$Bf|0ERp!mfHF-ae}H=ZPQYS& zj}%Y6k!;a1FNyCTo(fbf_3m`uzUs&(iGosAs=ydp%o2c3TfNuZ0V$-K3)nECN?D%8 zmyilr441u7K~4ZlG?VJ=ic!D1L)KaWo&eAj`+e1YfcXb;9d73?pnmEbyv7cH*NA_I zzFM%${Pg}GS+Xx@tMTw>$UW6b#}UduwcMt>V}EQT1Jy;@<#Rm&<5RhQX_eh}wnydb zMi46;@TkM=L?Tst3i?D7K}dw&!1Dqy$2jv5m_jnzb6>>_LZn~@C4coo8o>qWp3R7) zVF07JR(=Fshic}LGPUS2jizW85}Vutpze8(AXfv_ZqL$hV%dB~?JK;AE#*zm4dpSU zw}WJwK(BZ_4q(K&GEIauFX8k4pS|DU_d$KX!3LxFXyOJhXI6_hobrBy`gdip*9$b% zFE-P%X0`Zscd+*ekZsg{b_Om_@8BbGd$>2AO*JqKMOI;s_nNo)TF2`>`4BOWoqVUb za`1Y~x$5vzbq=LYNnPr4gQq<-WYHmAhadIPEO=_)ho}J6+x%z^wF3ZN z4MU_v`@;9(s@N|b$me!p+dqd@lEACQ2G&nSt|mmXFd;I3RXLSz!B&Iu3_s|5`DqNq=@%pPQ-RDqx2y#d(u7?%LoB7cxXM`qT zPP7!+YxiIva$;8)RxdLW00Sh=@fLMb&M$1$0;bzw)cX8dzyx(Vr)VlFl@^P(0?d?B zrCk%HUA#zl4y#JL@E4@(a6MFHsE#LEG>aq?SC{iIcC!4PG*Y}CHaheq-ywWu2hW#vZbnc;J+(XG7_GJke))a&bH>+2lV`E zEhjt5(6)^{oymk30M)bnXkmER4uTxNw=bUOa+EA1{u#7FfCBu_mZuuSkON-G zgvU1Ga2ul+SkQTdjGm478p5i2RLYA^9(0pOjpS3r$(Zj^bO|G@5iSas0q7Cd=Y!p~ zo&Bg2yEh?RorfIKDGv_$lr;N(NCE(9Wp%xKr{XEoks0$ zMCD5RZVFZ^Xgj1ro2?@lJ1$BL1V-xS_jgM);(s6-LeCKjJVyV;&8T!FI&!wkaVyFN zfEby{i%9dlk4(u#kFI19d50|Bhv1VR)2n|X?TNrBX9(WI9u<}|Q~cn!Y2GA$&vrdW;i6h1ON>r%@ATtAZ`dj+6Nd6faf8_IZDNGK!lZI3WcL` z{HEe`?Q?`r;d5N*`W$Dd)wfV*>I~Eo`Jm_=PJ5|m2y`5frvTM2_|cXB3;@KIN>XfR zt)z>DGR2#E5E#)D%U_QfPdf~3zj7B6NSu24$DE%NiixIDy|E&$t~F{W!%oW3ddi+T z=%%M!)gsG69eS#ffe+&R0usSs>Pz+1Pwiy+JKa*Wf`W*eBkH?$w1c-!BSVH^GNI2% z{T4gg4lh#^?bp~bD7``C7cEM^lqQPY1B#N=spZ&|Y?b2MT@D7DUli}F9vjaNGxxlW zd8p}=q25EA(kKq&p@qa5&#jtTOh$I<$I@(%Z}x7}K-_X7OHYoBR7e4bGhawZBmGegS z*5pBK_gx=tOKu^$)A%d4THjyPuYg;awvu2&@-HNiE1R;NAS3<+A!FIEdP&XIznptv z@@s5yEO_1If}<0OaxWiJDSlJjnlq@UV7*bYwaa`jA+_@n+~h43BJ8werG(z+bTijt zBa+%)r*4r*HCOKLiWgbyWA+iN#T@mtNI5u>`8Q zB<6C@sZ6c*#mZBsI2_`e!iMBwemXV5E%mH_Rcek~YV{~b8f z;HWM9`RiZnz}KkVCPg`l%W<$6DpOM@Crh?Pp2ErMVOL-)uP=A9-ePDXZT%7_>+g0n zHD)FIiWX%oM%UaT0iE0{)k6DQ)N9jR_v#-&wtMwYRts?2Yj=K$+dj{A+l}}$VrfqP zBt$3QYBt4+Z*?b6M;LAcR8#rUzSU#^ORthK&we#8+PsXHP`yINo{p)VmoMm2+P2#^ zTQ))>FAQwOq|2X-x^~hyTh~ZvyA?;?v;~(bdeQWqZFrXY?G~L0Z^^OY|J+35?ApYd zo@~R9IySt_wc%4TG;3@dUOm-m%e?>yv}?mNvEgL|>DEsHILU@riU4)9;rGBw<_#vD z!iHbq+VBDD1V=u~Qv+<5Xatr;?IXl7;2!|0xA@T%-VOj(t^r>o=~!411O6g#XT9Wz zd}ocL4Y=>o&IY`cy;p9(HELh|7Y01wn%=GkJdKmz*?eoznoFrn4pgfgNp!m#M|}tG48SLP4Ms0{_yIPjXP{V#zyO@VWPYsr9Zw zUM2EW*87`ZX1yo=E9)Iylsexv*#{uazh$x?rlTPET}*cI6K*%^ZnAy2>c?!8&3?jm zd)?Qu*LB2oH_|(j)r596(z6H=BYmM5>D**Ki}a^3(m~fqyZ#X-e@QY#UKDvAB=jWj z`9vFPeP;m*PwxaEyo;ir7z(B4Nu(Rf^UDl%G&^;Hn~ePtA+Z#lI?GSO9i8O|z@0nG zNxfv(BOONkX*=ELa^gwOUw2B3c{)oR|gW_Sqz-8)i-ahY6!lN`>E5YBZP-j@WZC@6et zS0rWr*IF@Ns-_iVcsi}P0#IGVkMs-ZqfvTm# z@-LSeqqfIE$};#-H6P%0Ati%`MS+43hdoZ2gUTyL`> z@+~)n+?Yc`M7uBjc@g*H-;L(%+pQf8b2V-x65M#y&v$SG4lCc|deMaQk?(yjSCdz+ zV2`_clwl~&g3;GOl0xLPg&^r5C|DtN(gM|Xt(a+xqrs*0xULOdHXt)~_`cW|L zlulNEHg&@ZsW15nMUYf_ozn*!xWdxsJT2VLWfy2p>fuj*9)+1=vhvcQ=eY_n}M0b&dN zU1Dp#EP*UZxhbW7qY&s!s8G2=!aiYyUmq6Uv)aO^BZT)mC9LB^*|%LN0-~P3hT(#( zOFKzaecNZ`y@0&Qv}AkgQ$%XgV5iG;R!S@kw2R4{4l$XMEF+(~nbPZP3D%dmom|h9 zgV)K)V%c5`lqER_Vl`XLHaknCbV5+23 zG0{^Lv;36B1UeOSUk6@g$pXmLN!$QMpE$NeM0++lwQJa^-Y=dz8T|sMUc12#Z5>iL zRH%P|nziyMOE$?wP*@gIE6}x{G7{l8;Ib$Jem6o-Kbi7u_mEE|dQt zsr}*kb2FuYrgDw{ z4S&VJv-ZycrT7XgJTAL@w`?;??7v7eH5lrem zFIg6mMO{v@_P*gUK(dVXM}o~PxeWqOtqG}-&4o5C6+m4Yh10LatR zo*+Qm|HlB-pZ`KM&a<9oldajz^5_i<9s7MjKMS-oW5)#LV?C;GtXT-k6U{ao<2loVp;;A5)b~ZpW#MS?`09Tfk73#44kM8U$f--Kp+e zh|X{t2l3^UQE|d|jX83$xnMCHDi-_AS2`6hcMi-}?<}|5LsRsPgejS+_Ayj@f@0Np zFro#G~iQO>`4t_%$DU9(5QW%?nzvBPb0n)uuIk=JF8A@ z)UF~rmhOr7@-*#;Oht}o#nKe<3=v4@GCGtI|0lVO4jFLmZB(yOAT#cS=C!@ zdEUlRkF8aIja69o16SQ#nMD_H0Y1?60T*13Qk1j-;-utE)=1_(pslBsNSt(+`d%cm2P zLpL4vZr^kycdfaJ7e4-Rpy|V*n~r!7Y&x2|iR%}pOb%D2%hl$qbU_mug18w~{ncms zh>l*#g1#41HnY95<9Sdeeb`eqqROcBGf>v2p41$2)2fLq6VR$(^QNa_o&iQxwGY69}7^w?^J{XUFyPSXVvI_Z-ofy>3X#(Gl7>q2bN}e7@+) z5U~qF6NFl_p@y=;^yP`>`r37LLA{aKzB7u@fO%BD>e6XFQ05=6wZC_=zF_q!zIS4; z7xv`?EybHi(Ehy>`#o~WSUy&DeocXV@8plLxuD71%6Co@W%~OrTkZW%*LwLJOvxtw zy_41YdnfwZFFu7McU;MuFMKlMKPLZQy`U|jgh-4LznsvTF&7Y680~ir-$>z|rW7XT zd-D%+KC8F+O8rJg>xuBj*wNFfznTdDvYx%7>DbXB)%_FUm+IH)motrrw?jy8Q|w3| z!HPPQf)I#dXm5T1Des?8&=TlAA60KN~(it$mH*KCk+b431D7rR`TcAlH{MHI5sMa^o zHv1w07AWPGqU1%wcL!Yv$T8;BAbDq_Nk8>9D$-`*T!8q_m+)`+O6ZByMbbEtysm7B z4A8mSu5faF1fcUI#(N*`DN?AF;aVD!Kp&wcv;$;r*XU)cpAUk7Em*loTe_F==uM!e zenlBodVj+ofy5^nOfYHutu}EA#clVr7Se+VkLxo>P$JOAg8id|q;+n8Ko3iQAQC49 z1w28OO5q_{d$}ISKJqL^Pnu`;5)$(*H}c0NTd91|WQJKPo6N&zscl|`vZX=zBebEO@D5gcxg)7^vA|W+vgI{sl zxl9mQ5Q^v?j>`hM?v$n?&9h(WuUJ`bGI+2E4ai6eiy!ovnUZt(}@Vgyyj& zLrhk52r+ZiwEOuGv4KB1MUA8Y`&VmQ;0raj(f7nex{`t*e1%}GT!%>fPQERH$!~g> zQ(L$2c?oB~g1sM^^ID&ba+KCS0O#8hdnrxa9oe>7sD%)L?E5{gUx zS)ern{l@4^8a?%Po=#K0@hHPiHhHma9EdL!bOanGK<%p0Mv6I@k)Vta`K(f3`m!`f zn*=fs=?%^~*5&&8dB6G)u&?9&HU&-wE;kQ+l7N(;b0)KM2oTd!+n~G z5r>JIwGp*ujt&)XO!uFT*&ndVoT8RJ_(>=dDUF zJ>LK%6=^ND$DaN3iEHN*-t*MjRSo%=`gsvIlP~_Dh5Qy*!TP-4zWrPGnENFP4bT04 z_=}FoX+=c&*yCzfjGW8dRC+fvH*G&G^r(NPJf0_4fG8I>Mg94WiMx*9I~6b-ab=-8jrS@Ju7%YA z9V27f1K`lwOER_E-}~%V(#@2_CoLzDfrAoWB%_pX*5Z9NlaWKmsk8ao?W0nN{Y8(~ zQcZVaT1#n^Y*vdc%oI};os^iQT1%7!symqGlw-?gb94q-#2D!Bheyu9`R~}W)SIJ! zmAa7DQkm-GgCUfwYoM{`bjf)Z=5wJ0Njk=p`aU}K2PPV!_gk`J@i9UvN))X-NvXzN z5~IGw{AfGfU3&WYRMpki_#n=~kQPdNkWol~>$ae3z1Xo6YXGd1NE>zv_2HdIx zLMrSAOp<_JE{!H=J=9O-6RH*Tg#fWNe%*9YrA~XKhfc8g4B8gtKQqzu`=E&7$ujpM z_HtR_!vs{=eMV)87DVGR=a6m0wXS6ZO7f7oqoUO}-s34GS5BE4%Jib>Quaj=JKEfV zF&wV?(EX%ibA=Au9K|7|FuoYiz+RH&={o6i{e20?x9?;!D zTV*;|HW&H0>=Q%LDy(Hvq@{-p3a^y(BF&=!s4v@IpUzU}@UBh&Pyp9bOMBH@;#`4z zGFJqSVIY;rm!=D4s=(A&?gwa`-`ril6q4xmiLm-D!!nwLl(_Pd!)Io{>#MMC^jC1# zOne?BGpFq{OWMpy+wyms52#oxXA{i_-nbyz*7R{kV!>tlPS7iugBwd{w#oP4dXtE)Zm%0FcgdIb29n%P)~}QpxjC~>)4RR6^M%VU z<*L=Sa#v;lE2Bz^8l=jC=62`uRKEMB zFHd#9`?jmXeC5gqc1MS%{7dr>3Ur1w?Ty5ww=S60roZ-fIX%`Yc}w4QTEWNO9(a#N zN8hk!wp}?>Irn*&yk||?h7ds-_p7${UA6^>BLg4F;%lSaPn*M6G0ZK=|A0`faz=@( zueBe*4cNSRQm@p#Q}e;lf}@fC=4xZ*ST7q-%xMmtrU{*n8Y>UU<+K9}j+#+!k1$qV zJXTV;-y{4QSCJkSIKrtOnUY4`SS+B=}n z0n`2(A&fyr^fw8k`;r@3zjW^1d_bN0vxGvCl6}cPlG1kmhT(&V279ZuboN z(O|B8a_1F04j4dRk?_e_4>_BEtJTk&PRvy1(J56K;i?R^VYny@bX;_}RnmrNfUr49 zi4;%U#|IU5j=)^#0l~XKbjblP2|3MPd?LfuucAoj>H2NttYhuCbkaZj0_L0MB&zVo z)*pw4hhw{Yupw83{~1bT7wz~>`RfwC!F(Ed8VKw&58L~Dt(p7e>$g4TmKiI9+z_t! z5qr1G?c%eHm0@q!ph*OA)WAul?;z(A+Hemp)pKtqkF^}v*`vQn4P`vR{3=(;1`FLx z*V~bbZIIici9KGE@0Oe+N-?UW1xNhVcO;4q7qrZqAxC6q9l&`RcKRIsIHQJ#$!BVu z`CBA5`*NdY=Nrbbca3sx9DHN9G4(BDD%TAzj8H3Ss(2kYw;Jf%-vvaXJ7Ke zqIA87a`&@y(`P`A_VR>7Yjcmrj`o;4jgLCE-=nVYR-a?6yrJ85>obj&ziPW2}8SRO_4~1TV zLf%~sqNVtz9TV>TU8%4pNpJF|J~7v`DD@^Yq21&&)4JJbeb;YJdavMEG-4i$HG7T4 zm=k?jnanUDx+AUA%&mzM8m7rfJ+?o`yg6TOvH4`i`jE*zf;cx5@fZr<3 zuJTtNqL%brcaRoUiKwcRqQ#(j(Dp4Q`#p&VeTixBnfog}LYisz@j3|nzl>WWYA$)MK6^46BUQ&VHn1KSQ-4q_z{DJmPU=xO9jM73Hst5liZSuwmIc9$#8ef2Y z;H|on-7O`1Jdtw~4{Ve9LUZP}acFk)Xl0HxIWbr#y*~PZ^}t&-5A2c89n?Iq+Y|A( zP{p&})HRP74Grgx_0;U~H6}B1*4E2_mAOajhHPVHWB5iH8*`cK8a}xEtT(={Zqn}P zAWW@Ig3my;eLxX&F8LR&-SJ0z`TRTT`*o|#SGzfqZ@%eG&%ep)mnhlih`#wXNA$&t zdfQ3aKA|-5s(00thS8znc}A*^Jk$F5aShgPz_f{k)S%cOY{L$mNG1 z$h6l&zuA;~q+rwiw|L*IzAcf-7gUY0N6lm2oz>r{-8sLC+Rqi-73L2ks9rly<1R`9 zt%BJWrIH$;_RuEnS$+thm5u0+=w)3@U#FfM?W=q48sVAwSw}-2&@kOgdNGE7+CFvd z=FV;Fz<)#ia2dGWEgtdZGBArFEG<3r2=ufMGA`4XW*7gz#JvlARMoZkKa&h$0t08j zfI(9U8Y_{9iJ~$PB|s*CB^tzlw&0^hno?U4W&pVaCrko48BQ;^#cEr7i@mnBt+rHc z1WGjln(!)wM=>CkfT^Bw(l($40+{)I*FI;G@M!J*f9~h==fmXe_jB#F*Is+AwE-^5 zOdK@QEi=XJ-IrJeIz{ivEH_-=cPRyDaWWK-Fi31YUdSu7rgIEJ#&9{&o~Vajg7ZPJi1yIrOKxmr=r)aH1WFwqj`P|^FP}}BX-q;!5=lCDVp!bms3bbL-Wl&u@jKshQ0f5Kg6WPb; zF(*|E;n`dc-9caH{FA{3>{Kg*FqUkk*jaGJx-Mfpm=W2yMsnOqj?(S8%yGa7^>q?2tdalS`tVZ<>jQS(zgi#jyRDDS zA6&LRWPfv8V7NyBP+_2ljiR;$1aA17HI72lUdqS`-u~iCK$L}BJ9DhGa)is;oE!4T zDct&rK%gX@0rU-mC|*!Sqfrs{48ag77D$4<>tUcN-p%Z-GtIweGG4`kZwAYF&BtYG zxGo5<2gH)~y8jKc8Ei)#JsZoO#3i)%Uuxp7k) z9hjG94*9DEt!0hQ4Q-qAWdl1XgDgo)*YYRcXx^1*;x-&s6Z>%I;e0J=(V*y`r7f3K zDlK^N1v10fUHtjvyvl>W%SZe>{vo~C^h*bmAhLOpvPNF_8k)O5x{~x8YYE>Z>$KFA z6s3qSt7&>IDYopjdY%cEyW8@xVuKSOv+Y`lcD6B2Nj zkDD8O;)&qZukrLwa6s&)(72p{`?X{|?$|YwJdmcBY&P${&gLSG#StmlT(MuP*`mfv z^KW0(NBRJ*tIu9rRq}V;|9r&HoV^n9zoGkIj`(*Y!81c1WOS9EI=t4~@Af&*t)HK} zBehX0*c0p(=@%LA(o5FGvJ+{ZyZ^-fx4-!3m48WX4EBulg#{ezwZScY|CBcCMeB6` zOZ6NE%_pL$`qbyvLfi&l2qwicA~U>@jRO>K*O(j+rsnQ9R~?th&>JgjH}&9|P0io9 z0l^`@9OhPU^wM(OzsB&d*8Po!pP~;M{kZf4{&6!4~$R zuK9HYt~Xyp2p6DAKojLN*&gd7Eq4ewKj^V9VQn~o2jM>%V%h4mx9KIdw1_vg{M&;! z%QdrB!RgOolxznUO1(7Pyw~CjLakL^MU;AfibxjDbn{j#?nUVibJt7t%k;3{7MtfX zWakj;7&ib%Mdkwh)`js>6+>V@qbOwl>KY*{&7P`r4Lzl(hDmCbTvF_B{}GQPNcA$g z!_UB^?-PE;&7s146(Lv#VG<_}x^KTJI7#p&K5H;g$rM)TBur2Q#zyYr+J#fR3)Z)~ z8_bT}Co5jv5B4&5Udz2;(2VsqYjHu6*@llch|j-UpXAXWflFgo1hp|xT@-Q^m?C$C zZlmZ0^ZUHOUh!^VOvGPHjkiULw%IlMx2YP3M1r;Ub>x>QR<0wbOSP89(!DreeBK&T zsM*F%UI)}{nwbXN(d@7wgLS{p??eWY)J6v3vJK8f9*|NNS)y|lcz|~XJCYfkH~HPk zZ!Ev=#|-u(b^duV%c`^2Msb*aNbX|uHX1`8`sD7Qcg7NzR?r@sZ2p8vvk2o?aS0g_ z$ZJTEZ1&Ye;-2N=B2PiJav|H`;h` z^E>HBvFCZoe!THf#%t36$|!w_@=Of2Tj)*3$sV7f@VUl&`O}Zz6i&j8Y&=HECvf@w z&HNG8ZNHMnVsrQ~wdsV*Oh;g9sH`<GDJyQwA?%l`Kr*d-Co%*Th5lZ3g+ihP~JJqfkGYCfKgYjffu^I7PLCD8Yg>T;5oP<-vc!EW+vwE4M@nR_}@E!PE#}@u4G^tCTMy#MyREgr) zbhC1<>MniD4@JDL@O+r7u?wC ziU+1f^0MBNJmqmH5J&Kd(_52+U)8xEK}l}*i=6iz@!R*xUy(;peuV*YA}UlNfi!al z#m$JNQ5KaAcLYbo^2`ZRorMtY!fD>hpHitqXzyWoa!;$owCrk0icw36>4%$>-wkv# zhiLY%B*sz3 zk~Eo_x#LA{G;33fm1$_c1R&!?0-3vFl?ebb|8tZqHjFMWI+9p*z^{VOw(`~-E?(hk zl!;|(4>HY&RLN&*5>2Vd^5SHN5oa1s$LCdOI!@>lZU`gyx^%3a3*Z*INz@_}@oK*4 zA(#?)uZO_aL|{c$n@Z^=@RLN~>a4R8cwah!7jTI-9c2pW8CKCmxbWE#8B|V4rrie8 ztfS89j77%%#WDc#eXJ8?Y9!bZ_!9PgU&4{JU>d6PFXOs`d4Qn_qh4;a2%(4RpaDN$ z+fG+LPJ49Bk+{{Z+AtiD8`TcPH7fp_VmD@hdl3s6Z1x3hmu4b(fY_qB3&NZB7(Mnd zqQ+{~Dgy)F^psx8c+na0Z6#hk*??OG%CL#M-WoQCnQ{k>xB4~HoJ9RXEXdsqYsABp zTle4`n(4fB89}xd{zm`GpWGemoo(9sKIt@4TKqj3-qosNyZ&X?j7y88RzhV4y5Adp#8g&VCXca!nXXFKbF#jlWoxc# zs~*B*HbCgijLr~eGfut!cdAWxAL*;Ydx?H>N&8*NAgk{?IN%&4J`t_ ziR_RJG^_1VFx{i``jlq7z}$n!3s!Pt*+cqHnUV-$F@{=3ASM>16Zz)#Dov9-v3x6! zHG_#ffjm~J*t(bG%x;AYlhk$?xr*x~-|JV*r&abRgI%A#b%r^gPQd-xk=l{EOFyHq zTYZmArq0oM6c-*{V5!}-#2GTstTv&D)!-|fBd5G5j5l!hZHb&VJ7bKMGtm>9rD1ii zldB;R9&ZG<4UElF;BH>ER8HMLr}@wylSAjlj^w|zzVWwy7P0jKUmLd`$7vqokD9*& zI1-o=3~7FyvUy!q(gWt+(%pcPtgD5bM}WBLW-$qXMOoRU4u(;HV1^Zm@Zs|$XC~GW zi|-|TK(ksLgy4LI4^Sp)V5CW!wN{#g_~ajHWQo~nYORoMeB>XIyZK1uJSQTG%d!e) zvJ^5Q)U?W963VL=7BmQZvP+-Ck}0GcH2e*c6|;zj@_Vp!Dm|ctELB2?DCTm#eAZgx zo7+ENBrl4l>i7Z7vvIYyo9dj1wv0Ke=dKq`i zK>62jnh*15MiicF+|F#%5V5`qFdmg9YbV0?gi65RBpRZ?dMb7YEo{cLJivOI1qObQ zi&i?ow#Jp>8wx)dtG7uzjJ!Y4V%eb5C@L+xO|PqKA6ysxoRcp{Run>NiKp~+d>BZN zVJ$?gr%X3r0rSZcy?D846*BmC7UN;J-i}yD`4yAu=e3F-bxXXQ$09h%JaEOtfhCDD zm#3>_BkW|fDuE^HGs6wdiPfSvAu`PICN_?qLz0Y6Lzg6X?TrY`EcU za4-3|>1>L4vJ%6-C%fLaXW^GwRu2Otf=~c9`8577@wC8J21UpM>4vA*;dKUGv6(z4 zV-?)=zbsg+c>mLavj9%1vHAtI+DQ5UU_FcPEc4raosQ^DqP^uH% zKpjDd;W_blAOJa%ZO$4+5y1&){iq{?OR=V(qaz^Mb!6ab*-Yhj8u}BEkq{@lpqXW@sS8V#3?M5VH9MQc7hQv zseH}U$%Xn>^eIv%TElx(Z30xp35w)f>Xt?nWIZA6vhgw{4yP7rtzEsewySz+ z@ca~5u+~FgGUs-qRC1nvpoB*$G$55Ez3nhg zP*~aMUWbPaZ}lgwHDEUU2;4}SbS9w~IXN0)jP-iygI-sTIZ8Nno~%HR&}e_~M!Ko3 zc<>sli*L)gh4OM}r(c3-cT`|rVdcNcIH_?+VjKhkiEqSz$vDUZjb80d$6a5%SBTB6 z5=r+1S4$CA$A2wh_zaH|JwO?eo#h`%TLP&epI@u|PvW@=NR}vVg^Ysai;T*8TOdUH z*2{!A_ZkIve;Tg;lx<-&vy-XUuNTv_a$fcLczW>X6oUD`uKg966%;Efb}Z?s)tuBD zVo&Hhc`Mx47T_)BNzQOXk_fF~X^@uzYEFgPk1guQ2zq?)1o~<(}h7rbUj^d=qPdN72~(W4(Q6?8OmyBtA;sBTnlMfIw!vI4#^A2ycVq@GpRnQ!B{I zdWb|p`7NZ69vsO03Sz_j4D-u?%1e{$XcO#^%ol}rO8v&$qn755qtr9=A2KqwjX4CCQ@jGJ6?Udr-Y{DHT=R*$7oL2LxKy+UU2=>k0Zzbqc_hCv5A+@xeLZBO2 z%v40V+7{S}c6284#CZ7LL!#s3!XNU=eM;Sj{Zsd*9Vqrlsdd6WVr zJFU$WtY+kSs~yHYaov-p@2jY#IM0$ETFv&@Qe@CR&OIGEKG>Hbm)BYP`9K=WOD0Hv3 zg9-RVNaSV-73BSK$Tuu_6{7xH#Xviy@o?Oj?3CVvo&_am8LnIPcQ!rGz-PasyRw{T zh!~n1*J_+^U7}L>*3SWvgi7ICJCp~N!eScbL=Xg(LO)LZtgTY`T9OvJL*m+a#c;)N zT;snGe3|BEQhk=*k=@3H5Wc`ozQ(`}K5(Ek+J+NtD^W-Y~v(kCs+D+o~-bBHsbSa=%J^R$$Ju4xQ^!wKh^g}e<&wujyzEoJDU_gW2WmWsX%+)>dR zxMMfs_&^-1acmUFRXAED*;p&tTev?1p)A#`3K?VKX02FVtRikIw@~t@D5|CQ!PS~X|KLIbdFvwvZkV9 zzmb=tRsEi=MXT8vZXc;t9OrNo?L$gY(MhGS&{9ghmgxtpxXYtIpkS?{o*a2uT7|$X zY0x&xxD}xqkszt}9o8xi0~9P_W16#~RuluyMt@F7)wWQ}VBe8If34f#HrG+KN({OIZdk zX!kMJ!~#h#s0kOjnDQ}M(~Ft(+i*)ZsmMz0v9gYMQKh||Vu>mroi#y{ON|#N|1#3Yx;vN*xwmr4nGinaZsJ#IPFZ$24-kdm?YqGYYY-^vweCgz zM2f0q!J|XIeZ&q`+#k4Lo!4VpsrbynU1hyWxZb9Op1Vq$)4jBrR>LT5`-r`&tU(9> zNLk`D54bzqX>Z}Bq%ex9SX`sUPuRFW- zcMGx!okJ&r3f9oQt4izNRhk5tIbEflp|mSBabfr3=Hltz#N(>a{b~SJ6D7cWw`XU+ z-@IZI2X`;Ce`gavRHez|q4w-bfN6A<_8(VnVk6f~oju-)r+X9sstWyHSD_MMj_xXS z`4yYEtb3VH;;}~YV^!MBuF@pH{D@uAV)H>ROZrt(2xrctp*1mZS zPvNt8T+Y%jIqbT*srP;<_V-%D;udhq-z_cXAH3gA_U1M_jOf}fr<};sNhz;FHsYR^;O)Z0fz|eX z+4tVa+Dyz0)8i6&cZo<9BI<8imCNA3$XrVq(zkqs6H4W@E8{v{N4GWJXz@+#HlL?N z#R3U_jr24|!t?1K&K8{f7pEy-%h`tWlDpT)gYw*y(TUnXsF`~b@_!C}@a!-PeD zKz*cLXO;EOE1PUJp_MZEFE1>@y}(7^qybi2Kfxu?qsCQ=ytE;TIjmZtSi=>`QoD5v zB37|nCu9bN2`&wBaz`LtW`L!;8-(q-5xN{s_*tyos8LEVzpWQ~w93CxeWE}fU3-0) zDiHpRYFFUNZgJ09aa1OyGnubR-l13x7u3%Y1?HK0rQ7LmNSyT$ z9!FC=X7)dLCMQ~iv#anDd^AJGuZLC{gsO~Akc=?F0PM*Cwg@MRZ$W>Y0ARbCkz9F|0axs`h=_0Aj2r@M)PX9I~I_$FQO>Ra@N zWPHM0T`c@4H*Cds&<-w!p$f}sac^G5E9fBT&#}8g?SBoX+y1~UlYEB)pBIzrL!u^a z^&R3d_JVqB4fzlSJu93jH~d<(X#8i%{@+rYy)vHYS{a#p)E&S&3Yc$&3W|pEk@X_* zUy-F&(JOW+y|2PaVMB4Q#%Wj~_@vkPWYv;cCQV7LSg=lnfF}_G@Bj2lL~6E{-=$E0 z4}<<2XAF%iZkTJBmhz;qFAbL=gEj?YHFRTS@+WsOp3{gd~_;yyZ^pCu1RHI9Ob1aqa}eb?&j2!6yXBzQlr%{=%YFeYLmqhY{V3Jmq2yrrEnn z@6zTZc9qP*vZCBJE=r&#t#Ut2r6_?E{X9oEAAXi)*EQxYT9A7>`jz)c$7?(EacVS8 zGFb?Rn@>S*QKtmWg8j>Th-&|=a2yfcylkH05(Ea|Wh=Q*)ZNB@nYPxP`+ z%o_y^67grZ6VKxN=-gGBcM#6V9j1qmTD++^>{@8=4D4cTf&%7&v3M{o9`u@5v(*83dK)%fI$P4&KS>`c z@v7A3ZfFgmJQ7Kzyl`uJ#P1ay*`Sz~$rrg;`Av35`Wb$7T*2yo5sfWs*cC2Q+%lcG&2y1Cdqze5!b13DvI zcF-9dX7~?ACQG{<`n`Pmr9>YDur1{#QgBP@AX8+?>EA_anlNFp8R$ ztz*RS(QwsBoiOV1tDiE^e$$w&Av2Eda(#ib?*hz)UD z+$VV}!B0!Q{$}03o5uLJAx19#ZMwf!{2R;}(gu&T;bC0XvqP^^C-UR^B!@2TGTvOpX@~96OsSPj9r7=^@wtpLd+Poq4R;S*Kv~1{E0+Kdw+~0ye(fKjUh9qu~~E%txl{L zObSGtk!fxPYs|M0bTrK2kO2G}(&sL^ZqxU_yrzu+E8f<>cAJ0Z`Wo=-0#o8`lUXPy z1E<)R>Rnn&Z##`-A()MMlB!PqYf-+|gG$RfKsxitPE{m&y#N1cS$8cuyT6Wz8+DC% z&}07c-HUqM)!z~i`joPIeQf_i%9gb_gZVNB=ylI-RgXMB6k5)6Dbvwwk1Y3GK^*SJ z$TQXid(2}W2^!&!Fl7);Cfk^V52^xW&6Y`2yPo<#f7 zu;PTOrc7)2>@$n|F#hWLAzDtzU2#roi?7k^jFPsu1R9m3bzL-tyR6WT+Sm-e?yfju z4~ws@i+YTblWz%uqpsmY$F@uA~ALp$1tLY^z?2^}97 z+7YTMIw?NN6<^l4s$QN@Rl!|tfG1VYFgsWgYUr91L)Q|SZC*Tdt)Dq`FGahC?s?qy z(0vD|3|*x}%h0Wm0DtRcLuZ*y=)||-R+Q~MtddO?SHO5JdJ&4W7X(*agi@3LAYIAq zS{a??+3SNNp#qsmjsOh@$WEZS>;D_XylQrJ`$gP#w_m_1-TszDOSdBu@XsNp3--lj z!9Rc>&oS3?8_KqHQJ09nczhzMpu}!lj%+xW4G#lWY#vwi!zCk>fY;G%;TVhBgC^2U zYj23|1*q1T4IkRrZ5xhA@L+5-FC?zm#PB(HW$z8%9NTx1Jf&8H##Ygv^PGX6c}YR6 z6*Y%^S}>TGwD3l?=Nd&#GpCzBRih6EZgK|x7%4i6s{PMn^OQXV%m4+OFUQ4N1-2sN zBII7wqEK=#a|8{F2U}21x+%iDh7Zvve^lRdzqugMEKfKd4`!LW{{jcA$A%OctHl0_+W}VW zlpTKGmSmki)AU&_{zhcBCsN|x;K*dq(UK@XFD`Au)?)MP`>cg2iZ~<*J^`_v&jC9R zkb(BjhlDNgHR+ighN{W!W=c0P2OwTd+al32u?+_78j>Jjplbrp2aG`+N1 zCVdYrg|f}9MYB`6?FuM}k0WU2pnvqmB~GiGbadYfY7pJVrE zWD4+mG8a82!0*od=92-wuFS=cEd=NTk1q)@3PQFi1)@wK*wU%O1)C&YPEIOps5x62 zs9^tbTo&Q?It?O!jbAE*^@TW89#xTGr)($zaW)l;*Q)H0y0|D5)VJ_2H8${vKJ zna7+A1wdX90+A6&wnxBL`0*%OQRFSCoA}|5z39X|K!c8Ndc(>xN`i<|ieBa`Pq=;Xm8(S!R6DgBOc+4gL#o z*HEApl%+TeMQrbQK3TV)rgk^)WB&SQRwdoG9M(7I7l!p>b-p(J5U5Xph=Tab=d66h z+~GF*jfpDG6-nA=hMBlHS)$tBVj9QPP!^rtbW~a~8S^iyCi1`*zNJnnU39NR`Uion z^vt}G9P@0my`B972S+dBH;A6|Cee7d=xyi)azuh&K~kgXK)X4Yk$`-ZXAJIhC-)SC z{!ek}fXAC#+i7}HjjDw&i=_Z^=aiohF=`73v7wDEbZHg8!x!mWmEnvRy4Ae~m=C04 zZ!-(i^(O2PC`wPQ;5suUYp|y_cAwr<&NVC);h;Jby7#1__%;b}W@(=0Dt}w-hB~vq zQFKa~+Sw#foYm9QMugr(j;wLeeg2c;P7!zL{c%-J{bXoYsA|@LHnxLGX%{G)chB!d zYqu>qWhN%(4RxTQkIbp+CWrfX-vs^9_&Y2v0R8QvM+7qdeR^aXgK-(fW{saRK%_Tf zf+)hi{Z95?U&F!@XWfE_jJ*E7c5PyNWqirQsF^2h`zVuKC%O5C4!_K{PY+;55n_FD zblLW~`JdVL`QBE6G0Q5~tE^|WId<9eSz6tF5hdC?T%9Xryfz0ZCctt#Q$O<|}vqT#@gR$IWwVt;zqutDi}ICw3L@ za7)w|iK>&Z@LuO!y`HSxhZ6=#Ey}*LTlBK#VM3!M*F}@UwaKE2Jx-fghc$xn+M9=+ zTlHg39@NSylhdVee_+J}&;Lt=~bOkkK;-Mm+1KW$5!gA*|mFO9hqQRT8Wk$FBXRywa8Thyw| zHRXwDHPwh9J2XTSak9w8waU4akWv8P_;O699{ZKT8bXP9H{co0Vg1sBEnL~ph{_VDL&=`he z1sgVu2ae?fQWb14T%j$QTYnou=XbM8upC;Qj_puvM}8b((Ui1BN89tS(%jH3O+i@JAx+|3s?1@$B z>;BC`P9sEO;paYzqG72w__xc9n@20UV`oKADgA2n|4m>5#sY>(SF76=1DzAH7&Eym zc0rAK;GC&@i04xTblj*EZB{9qTn%g{rB?7pRxRJ4kBbv+g-C5(>7a_{QP(Zy1=5>g z7Qjrby>u!)8Qgs1)9YuLw{up2j^GB!O+H)uiNl%tN2oOML4G*bTY(O8i*p9rPtgDA z@jJO~mMijK5ol~i%8lC0Xp59-j?0LH%mXuOGvAwan{qoQ|+5L+CK-G&fzlO zxS!zbmLjZokc)1R9;FTv^IIa%rBV*LbB+|7w4ol3--ftO@M>rSNfCW+htdl_3VcAooaHG`03 zjP(@6-v?!O5dR5<_}@!CL>@U_XNnxN7=;sRq>---&;~}%VJqEStAt$2JA}+#&^NcX z96V27pR0vJaW*iw0#^cIKvmW+W!j2@FQ(6~Pys8>ZtR$Gh0 z7UHm4vRVyKY?}1XcPjW8P0K`Gg>oF#o<>g?apv2agh9JplunZOHk>_%9ae2pr0}U6MCOB$3RF`D5xFs=2LkN7kuz2m-)_Z7lw%jo3y2)~AvMh0Oq;G*Jm73MX+A=N8>PY{=wve7EurwS6M z>l392);MXC&54~UzaS|w!N`Gyl=VG(Gr~z#leq1JL8ni0>W{b@CaK##I_!~me0q|D zflyW&owz%>lq^|?zT6%Ay3}V?#;}PgOnOJPt}JuhPZSV|7O*tjGc$ghIhHaIr|1c{ zj=Q2Xb5|q9bn@pE=F?N^d&)|t{=Via;?+s30!Ly?gFmztexqU(-Ms8d^NT&0but!M zRfd&tuSdo@G7x*Wr?%7Keb(a&~-censgk2bSb@2h27 zTc?(dYOefwRQ?pppM3d~BY!gGk2k`jCW`;Qvo7j-JtuTR3!zz-^!iN4fvOi=|4G4< zVbUK*MLZfRZ!gEc&}Y2hT7%VG;Om>>zQ-?>4(P)vm5@dp4+FQMO$0g2gU{_t*?&E80Fh3B*Rhap&&?x{kV7}uBau@ zzq&w+qvJ6Mb+;&Z!7+DD?tZdYK=nGJ_mR$cK#N0iOwi9)J#HF#Xdlk(s5%&GsSRBS z1#^tW9nc$hXf=;~T6NS89BwTBwEA&#EUz8hqSY+EAe%b$z>SEC+@&9j%sxtF_{88? z4$}3>;rBJM)>YO@V9blI-TUM4Xf^MqeoS~s=`U(E`!R1i`)JlUhyxm$x=(=v&8&Qt zH72iROyO(M1nnVOzQCpl+B<0iWR`&iMH6to&aTs&U_+_l7pb`@FOngwL5~lKtT)tLXymGiQLP>H5g8ONk5_&y}<=b_D=mUC70~Z zKBpIL&u$l^mPM~16;+sphdblK(Yk*#1ictfEW`-j2HpPxycMVf7$b-$U=(8ZOFpjG zc50Qa>J-{)?~z{C&QGlS-2)$-?%XBoHUG&Q{4d-Pd_J{4cpWCt;6b$MGcp6o`tqSD z^fY3*`tX8RF$^hCxJ@O1VYf5D-S<$apTd3jtOUw&pgqFNwOuf}Mq$9DIBU5H0b_yo$n8fEm;SRK?C(hO!;an#J-X==kAM|gBb}LC(W_jV%+Zx z*d5xWzv=dNie%%ns)2<(uxRi_o!ksff;n`HhgceA}m z3Nw*}MDnDvFL_{$OAfL87x;SYX%6qckr&e)B}XVyo5CVM!u>xcK!OeJ&)_W42VP)b#BgZ)jBdkc8T1R$DG z5cAX*PH{#G^9jqYi-+sN%4*vw&!ptZl} z5zZSpF#grvZy`YNANHcFxa0meO-5UH0B6-5z&|$`y}nUy3YullGk*>??<6P^_%=(- z#lB<8$UdxN(Xyyj0ah2TJ^ymbDoriD_(q^qf4&(Av^EleARmAWSbwowIz$i)7NEP)hd{S%p zoXe_sK_&V-f&?>qs+ylp)I1*t5p0n=obk!`2u+eiNhDISV-m5s zR&27wCMRMu60umKa)cXP`p4l~SN3^%xp`GUwlV;Gwwv0-j=^79Akh>;?ZKCqg+M8A zi2aBFwdftur-c_V@q)1!)_`ll=OA<2MjWEw`gW~iB7H5tyX4-9jN_fSjkFZlj^Kkf z9>=lxvnU>XEYGui6*V{1;u!H~Yn^p!zCB^*qxe;dS8#UTMjj6Efhy|RMAT|4%9=eDwKNeWrdZ%u%Iuwz*^~EN7FQT)&PZ7^I^Q>T z@fUcnR%N{_k#!V~3HiQ53ZA2Kx?t*z|G0qV+O)fcSIo?G8?VIt%({GjWY4&4eyXg% zi|0q0OUf(G&rITEQeIYRE}N9m1RIOp#&j+)+Zbwwav--Kv{FB>AV*s{Qsul(JsEQm z03pkNc>|lRz>6FBc~!(k4cwvth(#0pZgd3E>@p=Lo9L>ZkV%3H%9=$+QN-YyC{i7F zZUp6nAJ{QucED}Nw2J$Is`aiYR6g(Ik_p~1_N{~C$tfn+SJV?9dZx#ijeH%!o3)x0 z2(%mY`tlDHa;twk+%bqei@g?Z6U3IJxSnu-r8!TLk5)opmVoWd=#$b`;hUx5cJ#$e2*M-xj!pwJRDef$`EXKaA${UJXWv`C2LLx1^bK#2D)QI zgtnUIiVeCfE-BVS-NpiFz9Wl+K$C11$AC5J$M^y#wW6be{`#@%>s}L#66npeVXqA#c#^t3O4z+Lq-V&;vzRMD^ zZzJrRsIC`sB(A(eE^QVLi1|A4vGcSQh_Rki$chasX)#m3&%PaOMrKt9v}ZTzvs+^W z67OvpKa>W@@}MNht1s4J9c+gAXFkFwz&P&BICJ9&5ZvQCgK^wz@rrTW@A}T@9SgrL zVDir5QeM~4kIXc`iEy*9Pvg)MO2KVm106iWgJKLjO8 z)~LBAT~QT({2@KKT`$@lDOy9kKO`kpX>UG~iCw1a5j;0a4(j)iz27}jbQ0FdF@!!> zpA8xDk@?+Jx?kcpj9HKxW|jCVm*J$4N});k8c*;wieAF`tfb;RLOJ85*;Axd#vGNd z-xMjnU&Q}XxGdxhmcd#ni})+X`d`*6e+-D|b;z{_ri8GPRoMn{1JR;YeuyLDUp>~p zUaKr4QKYDf^NsL}|42;n{nO0DyA&YGv3(Oku`edzg^bto-#S(tn z?Nn6ugB&)4gCpZJs70X5J0(qVS~0B8fE2yJAOG6w$A(l-%d8%MeMAkUe=X^XUeJTJ zdf7I8_V&;PXh@N+DN>?1yQ@l?s~$gIRq}C_|3uZ}r-Ju!BQZr?V2n)k-zZvZl+>EH zvs%Js&pUa_IJj0no86|ZIOPQS1#3BJHAn^iZNUS%@oN8xyrh5@ttB!yo|mLm*5E?O z^J5?Z+#kF8()$sDi?-=|wVIMsp>xhbJD|Xzq0Tw6H^f5`PHmMCVA?9-!GpfbK6Wr4 zxG}hu9xB_7!k&RoZLXNh*H+3UjFHwCy`PfS>|MqKeVquZgO3Aqox8M})KL4#D*S8f zIL)f3iZC%g5)tO$Os+kIq1Z}kl$)fjIw39yytQ?)FqP4c+3e&g5Vt5{HRtld$+ZTjD#(s>{J}NJEDF*u+`6nw>@g%p>yV`X9N#%^gU!KC8(Mk6no9t?(FL^c9^!4n4s~eQDqr=MGz93Ux%=gGu*G>mP7P8bfgKkG z9#Vlbu&E&3 zhAKz*0ot2=%iBke}Ukd;MWAcgI}qh;OW?&=UU7fgd(qfJB4zSGb&%m zgo=D0RknLFA`{XQbfmB`O=FqY9UB^1ng)3ryFrQj#WMa`$|L~ivmR^_@d@TA0eBJP0=u_5{Nad>RzrayFufr(pP~ksFgnwIwe`*wd zs=~vG@TEAAj=4bb0RA@s{I7+#T+kbb$>EB{^f2QLJ^Y@pqz&Ca9&%KR$p+=*vZMEA z!mdazGqc~hC%LRy?klQquGkUmK`ntHv?!2q&*VwKSdhnqTO(y{VE3GGeFuzeOjhz5 zZIir1Jk{ysl*}M1;bPc*!4^dsIbof)>J82#EVA6r6;=$J%U)sQMg}Kb)FQRGEVOwW zE9@2#+c~G!(MIsJqx>Va$R=xVUR{1p78&Oyd!326os!RX!RUtcm-SXFoBMWsxkrE4 ztKTb&>|R-9g)FjB) zmLO%ssEeb15X- z`~gsb@xWE?7;lAMF(#>U|7%XEJMgQROTG~;t7pe{t)?-LmEH^y&rx9Uoso=Z=P}VW=jZQ=}lYA?=KP;C-8kL zXrnX(4ezbWx8^B)B4YAWe&SxiSFW=Di(1a`oyT`IzhCof;kT9FE`G=Ob?s+poFB3l zc=dhmG55N)N?A0Vlsa~BQd-O-&^wY{M!}5?YBMi8jV<&B=kc&pWQNDLFQCZ*X#?9I zCv{Fd(;}17#}=jsr!a+)8ELOONit57JlmOMWNc*hq|`BmsoL7Du^a3H2Xz*hE~&qo zNFBfys@|j!*6YsLKj6{dK-<2?FTgM1PVCVMe3gmfrv46M&Q;%|r`)>)GR&#Ju|>s7 zaL`iCK&$q=aOHa|Bx`sUv+=aUoW9ITuQy5h0b|RKY85hf+<#zqqpsNt0U_#^Vp;iw z<18D%IT3x;Dv@*}(LTIKx&55V_=vo18=>5aDCJI@Sz?r(totY@QC6;xSk)zlts(FfQm1wYb=z%cF2!8AaBulxfCygg*Cs0Nfp@pbQe z!@HcZ-ktmvq`1yKW7oa7KiZ<|3NzY66<45|?50)RZ-gHsOlhaSR$DXJC=mqKq4AMZ(ViwDvdRhrssU$1>rij0-%TO?d|%hyA1j_clk zMaRZw)gA9uzSWVX+GN+GbY6|JQ{{CI$2tb~K>2aUg?Mu6&T_i!bfCuAsPC)$sDE9| z6FSiol?Se^Lp$3<+o7aQk-t^5PfB%}XBl006Qj%6%D@h2i|!I+#%-`1sYOj)6jS;+ z!`m_yL|am)&n;toEf!7!eur}uiY!Ri&kQR|8(Zel!sU!r#Ge+q$8)Ah-=*G1Zp_|( z=3Q=Nkm%sq=VAUdi-O~F7V?fK`X+d}rQwuT-;LfPtzrvc3wX5LGB+t-cUi2hB8%nU zeZ%bSsau1Yp|W&`zBE0^g=?BVAyemfhwYTjbGJqyrYQ$Eu6cBVWvYKoiGVkPr}}(%>`$O_b%%@LydbtLty|n48|Bg#$*N$Q z*&p=X?c&x(XbE@#(KPG(A|+{&@o9)`#v#kc1{pS;Iz-C!fJhuT;j*wJWp4!#fQZO= zcjO_=l#;j=@$X2Pmvc~DFWW9}LKWch?_Q%rq`K9iu-cQGds_QRhw>cw@N}ox!5IYb zZYbL}UGXTs<%(21$#D(&#*?sM3TlBl|!Qz{4)A6VUmQYkPNe1fR7~^ zki+QuPH3k_X5FIaeD5seF-w`BQQSX`Eul;O1ky{Gm$WgH#-!awpGEHRkaLe-Hw-4b zj5CaDEYkmgu5k}h8FdMth!yI;uv|heL3ztSpj^Y|y0_Z7$O<0oR zr9uSasE|?FtxjBNy^K6<%9ymVfh|G{)Fqc3RFzJXN@XE5>Ekn4(okr|C@;;EiBRAn z(%|FK)Nf8eM~+2j7m09ylXoF$`eYfjd~483nj4Z7j4W{J_qbzwMLOgGcifWd3#MU% z+~1mv2F?RcOW;qW@?zh-2uu?2A@7%`u#1Sj6cbh2V)GD(DX0(oalw3-R!|r7nUe{% zUWzQnr4)jP>vuL-k3w>BkP_Ys&7z8Vkz#mBWv!%ZVsBS{5xw5+C7tN1^E_N2NT_H# z6%MRs{E{HL5K10~K(c;B+51(vn)ea>BGe0pKPX_s79gLfm{qcDpYzIN3U^uOV%YPa z;0$jOv$J9s^&cXO+~0SOYZj!NYHpoC_y@Kz|13DN8Nx+C|?^r<}xWByWUnhXtT9WOk3-ukEL{S zhNZ;Ri`rd2^Pk;lX-ADm_*k2GSbxt6!O@@H$83gpR$}Q<96~6&31tB&Z77thnibyV z`aoEv^7$q~rmgJqy>EKT7`aRmR!l^>*C8plQG4c|_!LTo$C z4d?ZB7*mphe=~dhj3qQ&E@MPnMCR5qYIAhcaGKA4%-xi@?7J!R@FUEyzr~m=dwR0} z=)B>j`MpHGbvXT(0wn-;8qn|IG+N>+XH2(GGTG))1VCxIGEOFIZDnNY8R^(q# z#-8RAo#_I&q+!D3t9SISE_%;W=4ULsTd;`~Z5N|8)~yp)PO>r~P>?bmLIDbVufq}E zR$$>Vzd6=_5b1&jSE0Z^)ML?h##>t3c=5$nY!}jz=C2nsMQTc7yKMc(g@QWlJ3J=e zTsEx>m)PeK5l}^9%_Z2*=%KJ(m&u9*z8fv6$~-15c6_ngEqa63h~D5A(7nMt(aiXK zSA1GJf>F^wF?-c8Xx|?!eD!|Xs-KgE+u05M_-?FqPjDos{dWFs9Boc)=r2L@dt#Mz z8o6kL5!?+&yQImKu`vLx;N}K@ZRn$X=6M@BLhJU8CwMb&9CDtdTSqC5=@9X?ZSk%U?*Q>O;*BlZ&|BPp5%)YRyiME>TW&ACh~2;8 zYDtqL?!pZoaZeKW81>fj#_8f7xuKVY&l30bR^IvI9%xlvDehhy21=TBxb-4{x|Z>R z@({7RSMwRX-JOGXkR%QMW=38H&plFnrue{i*5HYUTVeP}P)gjnvn^Q$IJzCBC({x0 zM)od6Oo=+f6mki(UnowjAv@Cf-Na9p?Kpm4;3r}zU*RX>6T(?q#qT@(e#9@CIlK6L zxnVNXVun~7Xnz&88kMi`@;&=Vse^FA48LY}SlYR#f9C+E&Imo=4P001NR_tzkJ^~M zj}Qnk@yiJ(OzBZ(Kh5m@C&+qJ(O%YG+tjaD-;ZY&hh;K3pq{6iXM z-xn+1I)=+Hs37R+Lje>8ztCYU?0};bI$j%U4+TdPik{&YJ~bA8s`DNXcPF9XsIkG_ zY!sp%yYNDIt1TF;2iNQGMP{#$4ThIGlAF=+@xl7eSQaUBcR=uz5TM0+aes51U4KXF zVG#;GjvVm*)SX=t!N&MAh^?JXU+Cp^`w*A92R;g)aK@&A@5hJks=WmvfQkBcc7I## zgfkl&C+cXb)r9f~PjGaXt34!gwP}CwAUb%%s6V**q#ojy2*&*W&?$`TK?d$d*BS=q zj~E3+x!SeLZR|c#VUU{FyeN~wW5wWu#xy1v+QwS{O|t$Ko;WJ29 zHx3tYJ@*Z9Ukv}@*3n3&0Q-9)6EiRuDTbWm3)ADp9)SKgPT_Wngd5aW@Tgwa!X|E{ zxw#hJn~hZ9I%1r{N0u{-9BZI{I^mI51i0E%OfG1ie)`r}nQ&k_dB70A1%5)Hi0yUxi>Z*a&pmGa2SF| zrQpGviB8{n#Y5NpO&8VY+~Yg1RnFyjsSqwiW!7N24uctT2mnjv0-@X{b&CGRd6pio zsFz8h+sI#aXshaE+71_+`}ndHKPY?1!N?cVBh%SKHV=h305+_- z-)7DIcDlse?*?}Q9pWiAA6sgd7c2?aC;K|MO5c~l9ip?ZJQimH z_Nl*_(<(Szo&U>BXH^r|@7%AiW0^$%${*;}kF}aV35h@0_i-STYbG8&z%T*If|fu) z=lr7+5F4c*%YB#O$JjU8h5g4O%X40yses1)SSbF~x)UD#JXGtBFzgzr))W0xccyNM zeMRJON?L!HLrZIqCxwn@9QbJCOc@oWouW;E``zZ;XH1V*zWcf}kW{r*Wa9i=Xnpj@ zG;`yBkoNVY=y$~(6nAp88h3a2)Em`7$KJSZl$R-t$!Egt*TJ6l7~Ad=vFTP}T+-*V zw&bFt@#6$=ro)^l)4C>nc2@9O;*eV|X^k!i2m~Cv;J+MIWgua8i?-@rDP_l7RX0t5 zoO#$e<_=>~lHuwd%<}<)>9p&vTuRi!192Mq5lJ>!(P}IDLr)} z*&zo-x+%q8)J?y1GnY#4dX5xY_zs+g{%w+VAFqr0>$!4&C6DUwkEFjthyBa`-Wtp6 zUcT|UHN&m`Zlmxk^>>NKN~8L^{$*5vkcflqGFD_i^l99v-ZlBtxPB;ncBts9wD;1U zs|$b7QQmPKJFoHD!?~ZvwP&}5JNg)}4Nk-T+#iU77&Bfw(L=mj%R4Z}fY;k`5^qC! zhqoiivEatcTE+*`wJ)_z|bZp)}MlfrF1 z0s8el=B~l0i?Yh0RjeQx6t-3&_abt^HA>$CJG5hG=o}J{Y|#3#jkAb)-4#iDc0GWe zTyiviVq+OWsQx@|(-R!(f+C{)6kGzuBH#(FebIVuLo9xf;sW?k*EG2`>&^m*zDvN| zHSAri{?j~WHaJAd4*i(wnyQ)gTGAAmkTEU3fUU?2@ei)8s3so|kR>5Fv@JvJa$I>< zewE4UjR{m{qQ@p-0I}=k%$C57wKuZsxm1F=Yx=vO0f~N(-$(pH_(h@Wc5=!sqWs1J zc?tLb)pt_2@)KoB82A=*LKm6GZGXx#j~ja)YNV7uN^Ktna36W z&4xM8t)IW?@LF#_%U;pOM;*v<%3+~&bZ>^@FsC?0OWWtOnPOl`-y6A;=b|4p&#?OD zrAy&)TzWmi6jOz|K;(^UFmW~WN12ng%*DNTWpi{;%w9&CiESyvW{`QZDHv z#S-;x%R+x@R6ycH&HQ)EpWc`JQB2%-&;Nhb zpUm->_UFkjbvofYxmtVrL}PFLU>ghQd#2acd6PuaA3MQ!kIav;KhJk|142b)C- z9VeN3k0sG_l3ksMxLWcjBJ{0Q$EqH7M}KAY)p*#2%5FjTR-^fa&O*KaFLD*SsgS7B zsrSBWBr_Hd z-VjMq6ZZDS6LxN161*RK!m1jo=v!GUdfPBN&8gq9QOpz^RcF!JQ`+4-aeF;FB9nz57te`L<0*h{CSixUUy?kJ9gpd!V@cl>SKEy~M4R zzJj|GQfF_4HWxnGD1PjAj{EsN#&15qXZU@K-}m{!a&o-JPx`)--ywb{`2CIFIev2X z?GDiP{3WbkzZz0#B7wcs{B(J&`BL~S9Z|qNV<)&Y<0Pe`!K3$z+~baqi_K;4DTK|5UsP;cwF+r00-#)q={wOvSJ!414apS(2QnkijZvJ= z^PN`H8<{7rjt1%neCK(B*Q%8MH`8NGX5v!#kCEU=rp|3wf1n6QZ3Bfi<5Y|Cg z0&ELF}x6IBus{-K&@g0UP zw@oGVy4a8~qni~)Gr4I=7vx^zI||=}jy^QJ*=)#1JHb>L#W(tH{G0dsE%(@WN>P<< zu1L;y?-kmP2jj3zsqNS$7?{teU@f3Q~YOHQ=;*`Hoor5WMN5%1bg#0H#0owU^IUF z`)IW8GmD9&hU&wQ;b1#Q$kuql9Fl4B!-0wYpDW ztHsKJL+`BnyQ8kHA9JqkbK4Y~Z7djR6nZAMFd-A0g5{GUKA%rV7XJvObl4TIHggdQ z`fTSnBtx+#Oo8w^9RO9M$=7L0t>|se`1r!8HtwivSutM9E4PmZeC7YL4+J zxtR!85{gg=&*dq)m8SDdS*dNVJ&tOca!z&8d*ehANs`KW8Rcy%vgMix^flZO0JJD= zBoRd+vS`;iN)>t-$M;ed9O-ib=+8|SWjy9`*U{3j+4+_B@C^86r5&kyp-Z2Wt}h)I(=^gW?{+EuA1Hq|PPM5W zPYLx86X^>OV6qE~c?^`ggevhX{qsm}v~F2<$(H;}MSHBwt0e}i84#sHT=Op9#*xT~ zlpmLMQu_*yZ)b{qun%UOa+nuSBagw}5Y8&cjA>o@d*1%#yG`z;T#pNlXYcJ?KoS@= zs!3#%b-*hcpZ3bM0S1^Kn3WvZm|A`ua14TlX8>6Y@NGoi9X$6UBnpNhrNXP{Hs8j} zY=5+S-73TI2s2o{cnA76rrEqs3A&8BRvfh2xmlD#o5*p)06TyZ_0SfI2YpV0Ewe83 zT$Fi`RyO`u@m#y z2UgkU&Wn265PxGfgH&B$tR-)>Hg&7ad5SW-2U_i?B0A9reJBHs1$HD>G^G!>~f*he=X=@iawdA`}k&ojy6aV)Y zBG{GAhugEXjOSeWaddj}_h)GtsM1RQ{zeHKZ-y0#KUevGQ$_Y#^24?d=u zA6r%E&KImun;F_{y(XKloz`m`lS@nfjR04YMZ|m+0++njny*glRq)0{&az&&nXl>A z>vr>Xq4nxBUuRjbTg}&v*6T9!wa9v1XTCmVz2=&)mDX#%`C4VYrkSrVSg+&F*H-J* zX1+FCugT_Xr}f$<4W+%-Ys7p_l}?fNny*glwZ(kRvR=2Duj%UbY1cZQY&=i6mRaAo z^F8qBLf+V@(%Gn5I4*xVi+7&Ck)h?9#NoNz1;g*4#V97Lm;fdS9dVn?I8V4drsrwb zYSXjag&Nw2&DkT$lVom@`9}7gbn>`E47!n15d}QqT5hIUtaL-M=e|)#!F5ND->IWV1N30zi90 zX=)>9Q!gBzr(H4~Rb)|%n2V1Zuu(7M<6>97itAHRsLm{{T&8yy0|DW}KtPws+*YmH zs)Agkn+vx$&6NCUzvMSpSihfe`3RA>B(~mLvShVMXCn@juBE5Av_ZEllrSlXNVBx` zC(Ly;=$4HVu9nT9xkcKbKd1#Y=zmBF5=$+eL3)*Xeaw}tGN^t*UI#tiYDH?3sWeD$ zwjxE;>*Jl)*IM(dVSQ~ezoyEA0=Ai7o$A#zQAy5O)>ohTHQoBURlPpG(E7T}{5s3} zy3YK%(fXQeel4=T=9^!ivc9I7Un{MzoVP^#$u|viY^u`r0Nm*-UUA%~L&@x#{jO*hvnmlz` zN}*3l6nt7OIz7o%B;PQkU~FcdaNcj7AN?oeiE+jKC&bbxt zaz6_j(ZmMvcuKUFH{A+$yLadk2}J-d7TT)fx+Sy^YQj|x4G4Ak8&M)Euihua8T4P- z$&oVlF<(31c%;RGgCaJq@)`NC zq&DDf_KY(2mGM#9T$^Hk_R5;V%he8FMk#8K_VRch8s-m{I_b6E$z1}3FAoGi@ zy(e7@W8zN*hcJ-?p#*E4-iXMs+;OQ$`Yq`QF*)h7eZqt~&%qVo0x{zRkF}%&ZM-Xb zkZMpdB4uQCm4P6jrIbRhPq|FQwH3!)OxY*+=BFy&qXI8hXSSid+|S@MD^Y_C@LV z(EB)6%zy2X|B}EkLZcg;hTjZDZnyNXO{?Idj9@b)tqMW>Y{}bWO0q0A<*iLIp_Jfk zIsqZGJXq-W#X2E4J=vK()4Ad9@I5`M`7<9%T|D_U%o323#`C}AriB3q@FKT616rfQ zzBoLAOb{)Sv}41USIL$n{)@x(<2ePy@d0iO=Fv&%v7dqVj8WcMgX>5sd`dFJf$ zTxc*GVjDuW$74dZjk!Q$aX>SwY95 zLBO16EVA9N_aPQUf7{}zHv7AvIgv)YJq32&3NCa%iAAq5ga`0@*Vjtgsw zouC^(77xo$B-`D#eu*i+xSC1@^R@-Nmi(e9#7I^OF$T#d*@~m&7nN*UOnz~JLw`5p zh-eSGQ<{c`k4`ozLo5GWYK&B9U%E+!>@df!B2U{c zQz}wuElbLjvb&Ca;=`+ydrjbWC5;zTfWtClXE`@4uA)|Adu|ByMQ9Oj4Rh)>ndXYP z<%!%Kf-OquE{?V;!Awi&t{ZP>3ZyFeyWp&-XESZ=%81%y+Ek7TdgnXJB!++2mDhDW z^J3Q8!dt7m1^HeDt84t`duqv{F6S>d$#H_X-ah47xmDKi9DV}ArSqG@?`wXEidA=Qi6U#tlW1qM! zd0$F+<(&AHdZAreEK6F-1z3=Mk8kXK)Ttt4>sRCnHap-U%K%5!=GZthFc2C zEW{h3q_vO%>z;aCZ;yskjgAMXUYeY@$M6w6URjUnZ5QRKvr}!xuV+fp(A-3X5;=Ho z>rguo)j+!7)P|nbiq$8%IwzN&4K9pEpP7!28LO2)MP0ZB2}#ywtdY_kQu=y)LWtJN zUG!x2M4~tKQOW0!JTQ5?q>tJ?C2*5ZFm8}^Ns{g+NvD(ncQ6V~eE|bGf5eR)L%K@K z#y@E$2#;SAE1-ernpoE~z0R>m5c)GQ-pE!IPQ$W;fH8sb0*?OpFy7EM;{VBRZD)-M zSyaRJOu5+@&oyWR^Q@!;YSw)-)wXJw@yHA{iRd9X7eHb@S)0q_(F950o&3x;9sp8T zNQyMb;H1bmQ=op#UbXF}si1#A5!XFBK%y2&FGO3|t%in2t1dAzL0sh0TLQ5z!A z+;?wMshQ%!O_Cvvw!nojG<>x=J(hkfO*+O50?NNwcnHi>iQRjQ$l=X&hHhggt8Qbc zYFyHwGk3~tlX>?D99_L(by*Ox`Io|axHMYgG%g}@@ZUysy>qq&@ghG9b-E zm)bm&{ST#}wka^h_%Ie}Kb}CJ7{kFVhj1JWG+rT8!KYPtV&q83q#$}dRL(CD)SnoH zxZ>Z_XW;RW=!)L&h{D|lo|)Mly=;7;U85)A?szmZ&b!0xYU741au zosBELjq4Na{(3`nko3zmvs30HV-vX3B-c(f<}$eR!xL9--BL;+?k}_*dy%h|g6a-C z6VYkXnAq$ous2Tf?z4mmuQV$@9~))Q9K)L7Vu+TMnvo0WBiY+Nest33eocr|7u@# zSy6Jb$cxM=lUV}g*t`tE4k}0duJbG&FP8$5BS^r6&2U-a*9?ubki-_7f!$nw}ShSwxhHav0`n<2f7^{b;2e1apEa` zE~;;HOzK-+YxpOo!Sgj!WsSlp@P_hKPo!UrMaoZq8awZo&*B?9N_;)mFBc(a07*mR zwt4IW8=TbCE{$b-@4W>ti6zv{Ta7JPk=Ex04L1;1^kfajoV3r2#_+=3TG zTfzgVLG_2SN!?z;fOPE$iUmnz_a1{=&3Lxh+KRW~F`mRxlkO*YSUkO$7Fe%}AIn(F z$1c5j9$w57u$o4qbgR-09N@-)wY$S`^U>tk%9Cj7!21c&78vo=X^Z>IO7~GAmJvCk zk{-8`9y9t$(sWq>gYj{~FX=*~NN?&JolUt(ctEHmH|3_-I6IqiQzc*}GLLp!WA3H= z(zuTut(*lqCl4Dwvo34QbrEvBa@G}u93y0G58kivT5BW~c+#~Wi`kub#3UZqubJp{ z%&uF1CA&B~=Z*BBoELDcC<1fO%TB10st_NbDZ6$3IJtcpA3m-QhG0@ESGoAU*z|Pf z8=tBxjdc;ympa1pVjG_~U+r_Etp7T_Sx#Ts-`Ltl2h`Ta4GY)=_0A#^++p+f^PlYF zc`qCQk}mI{@b6`_XGoZ3HC0Ag4B7Q-B(=hX@j(~>})VL4A9vi0axAz)py zjHi}9cAa`$?c)(FZAbr`UQiW?6XKFqv75_Ba2CVipms?5LiK6(ZAAVQG=gG4Mc|dW zH~=PFW#}4vYp4*|C!Q<=v1vk7uj42x*cD8PQ}{8I zsP74moN(2WT_W0-{4nJ|#2zwRuG zK;`*OJZj;Wv>;rde}JH6#z*LfVdP|FP6kSw^;68O79$5R#5w@AivOgQ){Wfk`fu$p z$q6bx@*8ZlxvJ~g`jIRSEP==V)h$T)Y5(rLHfD_b^_IE$k3phR%40r-HBF!o#S2Yg^+CZmSJB*+wl+d77K9q7oPcd%B<{xx{XAj^t*5IiA{Q;hD?Oa(uru;eb zP~n@5iQPF}D>=DhIbDW_$lsb7BNSVVY9#{pL3f^M?p(LZC>HFozlDHb!K;CSD)#e= zMKm8!%pxQV37}9)WU>xfx&Ur<>Nj_({WqRk#fdAd55_ip0z6(xp3G;EZobKKx?@t*JZ7dkmiO+;J{+W6N z+}l9XNZhB0*vDwSFBY#29}vTGWn8=$bh$1DU1IN{i=4tl6;fj8C;+x``7B|zg2UzQ ze`0J!EfT2uGT>y`^$!U`p+JSh!@pVu3JCbe@5^WLr4A8aFZiJydq*Oy^p2p8-=t>7 z{5esS`fBAR6pRftm$29@aQd_C-hP3F@YM>AF2x#Y6X@~RI=K5c>gcVnIJo65NSr-w z9&=yUQ(Ra8O0OvgN`>m{(A#j+VPz4+PvdBxP{rVp!NuefE*vTO#+K;<+se2c|>lA(>~>T^4allJP~*bRoHF81su51WhK< zo%p1cg1Vq?&Kga+*{o5L_^Da4)XW%jnaVObI7@>?N15JfoNgajyi%-l7q1oD+{J0i zV7=pRk7osXwO6h!{vnm1A<1M&rn#zlVh;@;VOfL;0T~m3MlGXb-cg$InLucia*3QD zGtNDWxP6U(>5BR9_@wgtiPvaL-ZI2M2!JDekZz&i_nb9^u9%A zOp-DWJ-l(oNc%Z+Cbm^LT+#8yY;uUv35|P6&vd0NM(1*wug2W&kix;S8LZ%a=sNz* zC@r|%x7it2d`n<+5_k3J2J0}LXi*BCskUcsx+Ug!-Z=#HpW7oWW2UQYK1lE z@C4%z_^q1#4likqf0xhF(La{}lp@a%5}i$7n#2aP-5x{jn9I7$jo~knh2_c%n_`4C zU<)7KFrdIJb(2NXZo`PWP*SkK zG?4v@%qQPr&9@sfAd&DDb)mt*0@FnHD}WxNFwM6+!Y9$%m4pPdXoAW+B5&j?Z>}ko z{V<(9{gOyvXb`qSXq@?m^H_IZ-e^)|diEv5bb;wQHa1<83=i|AP<7-DMiG8%%HE4t zxXjlj8S^9t-67pM8~=8DkKWwOO~gc#(Tnt7xBcy>*|=D_xw53?H_I7>LW?b#yfZ9K zVL}}wqQrv=+VhR~r^vRI){B|g%+KT>r6w|e#&^x2!HTr||7Zq1bZ_?zT8rPBL96h} z3|cIorOAsV;JeKr+D}bQe)jFa8qD!tO**fCzPo+$KXAi4uZPHw`@0-R>*(OA(#sHT zP$gJB8|)^#anXwm0?*Zbm|*m^xrM@Cfgk=HMm5lYg+dTJDEwhSJxle+d6bBK9iVCOFRBgPfX1WTIaLkb6 z3O}kH{HXfavkmj|7bqPe)-^x8*ra4X)^vomYJw$w6O)$;3S}>JHbd=HNSI<6+dCN~6ehSCTr|OkiRm z;p0S=2Kk~U&6Upk#N13|tvHME%Pg>|@)jCPH_IVooB*qC0YuC{S)Vci$J5;|`gM5V z`WKmd;MHej)+$Z_MD+$pklyIMHuM_#nK5WmDo5VP7+PB)u?EVdF}@y038Fs-JbaD+ zEVuh#k_@%YTDeFjgPCuO7xO0-qj@tnTxG&uwZx&;sg3lIG*_|yo^$`PQhFbeJkAh% zq#mJuldbxFtn99K-&a*3Pe|FDT#bC zn^OYg8zDz_p$)GV%FXu`&QL!L8D=+aXCA;CbOEg4lo-}9L!k^+f~U*`{v4N`>>bsL zcH^##;SCGH8+O^J07dvfqu>vzDLK+P4!g4Hk}-}ZKQE6N1k!(G_!N26ID8A0BUeBAO#=DF!1@kddCk0 z{2oW>w2_QMiauz|G*zbTD$E>Q8wK2!3in{TR#8k%pnN|$-GzeuZhV6Dr@)t4N+R@u zbdMvu(_0$2-xV0}g3Q9*-VByQ8X}CVE;iQCGP+jv6t-Zi@^e9iVYI#Z(FBPMM|gBW zVf0;94-}D}(@^hELdV(C=`|OYEu6+WK!D8MH8OLI7pZIv7OQSaf;@BsAw^EgHCl62 zsoXoLEKsMWJ6x3%&ou1|VspAHflfVa*clh@Bgv5OYL=4N*fv$caXN;=NITn=!bO1& zsMV8P^p^vjNZX{KVNy^PzVMUPX@dy3QUYe<3zr+SRpRPp5`L|OZ^ak7j~&tkd3vnh zSui8)Mn89Oy3PJ*9M-e5s9J6n@fm;|hVXDamWLSqEUn^BmL1DSD?iKS+Wb2YYdv&C8Sf?C!R?+ z_X*z4-zc<-HmVcbEXJwL!b*3anT`$38i#ZCi}{R!Xp~m{sY;d>h5FNnYB$Wn)7`#1 zrecG*PSqh>!)saA)?hrVQX5aJ2gh&@3ow_|E*7ElX}f~ql2~8YiXJyvoWYaJ&Ta9LWT<8BZhJ9m8eF4q&eTvW|^>1;7WTk=G$w zkEigit~6?fdxjdfFSIUt3-QR(5q4<3ViVn?zvRPhswl^$3|la$LQfSMXLp;!sGnV_PhD$r0#Bm4akl&GV=e zf-VZsg0?)nvGD`57N{`wwgt`14v5m!cB2%DCk}+3Rr9|lrP;ZayXgs0a;P|cbxyHa zqS|f-byT;4X?O%5*Flp*CvbFc3yKHw_QjZzv0wT5y{w8|N%KJzE-;+*oQ?!p)U(F0 zk=WFu>6C2f&jaMp1>e$o(6oVWt8)IM~BB7I|?Pc4wy$oNsQpdVwh24U?8fP?@@27+;x74SJcYU(#3ey z-F^`scszze7L(Z1RcyrpFa>YfE}1-v6lIQ>vEdH2LKLgc=`rFLdB?j|sCn#kxy(O* zhw~XZLqR2b1_#~T4m%s&XW)YAH2xhd0R#AFO6d)6cBJ^SZH;!DR=$b`q0gh(lE)PJ zyz5z%KRTj~843dQ(2Cj!u3ULwNsg%So^y9qi-}TsxX-s;*XwYV&oPWBhIho6~;)4xXdfbcBF{AI-DN$K)y(BK zV!eY%NsH4D=PQ2iH>Bg8a!2wGSHLL11bK{CJrDE`urV?00-SaLW{igeq7x8&zOhj1 z35MM69SRUGSo?`R$2sgJB$qFX#qhGtVY9uE>>UAL!glzXJ=d3=qz>D*$9b;W?#M~- zW`wf|9bR@4h-cgOctk(V2%bdWn~usB(Z)p5oCMEcS04<*C5d{u`Nk-k6YgjF=d-7= zal}Bk*f5&oSJu(opNafdFu1Ri$&I2ZChC#n=0tr>!tCBl0u`-z0>MMLP4TMrw>kA` zj!=IpA5PCB6Kb zeB*lZ7cvhh@G0<%#|R3%R*B0(D4I>q!HTWC{tE$(RMf&?g=Cs<%rc_}DqfH>4wQW=V{yDL+3OPfbg$BdldYk+dOK7k?^WSD zd)&8KL9-{zpddzU#*6nP_;x$=W(ZE#<-~hr^d8iY1QoDn6L6U&_I2 zybM7%oUZN{T`|X8FsAU0Gm3CB(k_KjcrA+@IQMlbYSiRGF%Lh`qka@)75(0u6wUe1 z%-|3=e)_C6x>6Z36RaX~&iYpYm198FY=N<9j(MO6WCiXC`Cp1q>+lXd0vPBYv4>3M z#j>cGgEvqiBdNA~Q~-j_6jYX0(NE6RQ+$cOqy~MFin@vM?{zZt2W*l#QA?SAHT!6v zh%oqGlNksbfzC_dC7FolWAe4~CGs`Ke38qG@vGTXI;6lLu?9(yO<(BLH#$Q9f>+p0 z%x+Z7BkdO9_-3bmtHZO%SV7|X;adW~ke&`y$cTm0j1odK57H%r^+w}n$zL|mDR?ks zg_?duedwGn_!673bl&h-ykU5HbduFWURth}MJ1_55uO&ZzZ9=gjH7n`wRkY6dQbQFCGvePL!$$R8ht3$uU)007u z{DghK$=zvVe|_dv^OT2cJ5mz?o&f;-w^VD1MC?^tA5&c0#{~5!QGv6-s0dhK{E3YH zog2NEbIU_xQ}hNC;x#r9uA1(_1IWtJzNFq^t4_WMxM?CCTr?e@kpc=(!C|qD)Ou3C zRT~X6t)w&Gmy=hOJ=f-2Qt#{%MNlcs{_%I@2xZAumIVL@%H%lZ7=IYTex%s$zv2}o zMu8auMO_q_lTxu;m_Gah~%rD|FO z6^iYKJWR*}UACbKn!6#OD4Y)p?d@(3x~qU(B~8B3|4yqrxdP4hmF|lxo)JLn?S<&0 zcg5(v6K;_e<20_~X>t5sj@KB0qn1f3o+<$hEkcJ0&m|94mjshdRrPrn>hJlhEwGCI zS`t0n-RZoGekt^qC9gVWc*s}OScqH8n5~jl27{6WOvx1cS3V1>2+-)kH%_uVK!uzmJw0 zj9c1+B1rj)zKOgH+B6Ft0hE61&)A#YR8nBCD_#`L8H}poX(ub98N`#_MGuy`u17m4*S89grK)Qd=)@Q<@0>x1z2OGv7c6|&9e;; zJK3OMUb8WDx>Qr4rZNwL`I#wn&td#-cbaP(QQ0mr;I^97A#&AZF zHx^$eohWe5#T>1iVr0?>{0MREGE}0z$pGOeJ>(>t%=a+r{hLIUDAsAI7s+RHjnY5d zhnXC~0uUMOF?*Jis{2B0s`PI)cdKLL)wj+GepSClxU8JY^Jab!_J(AV!)HeuYoz(vc$fa@FW+)UOZRbY2cRVdAh{Xvq8Y%j`qQ_ z(>_Wmk-nHT!=aVG!dqZqugPHxpGrjS>6C57kwPHma?Ik%QU=v}+?~=mvIQ zv%k9c51AL=egDYhdU2usL*2hQ@e3WS&`f>5J-Ts`tkVu_orb1LI*u|iTXGCEoUosv zuM1s}h)TMQuWx1(SAu8X;A1fiI!Ely6t^2qHnAD^>*Uxhh^@yobe0O=urStUzGo?R zmlKY^7;oQJh6GfGGw9e17cf`2CG9|NA%QeWg#|xB0SPZ5VDZtAsM1=w6@$FN!Z_{a z1I*8|!^()Qqz2Y_WqE^d1>&i_jn=NzLYNdZgaUTon^< zZ~4+9&KCVtWuMB$1B6k>7^L_%JM@#HvJQxONxQvuE_0WO`yv+uATR0rf_>Tb`UhHt zpSuI765*E?RFt7#EOw6>nFpLA{|+J+os{iHRjviTy@Ureib*PCn)ZT6Md7KbZ@=Q; z#v*prGGeP;bqgM5Ci>(IV|<6~^lDd?697dClnqxu4Q1z<2eb>yO5_|^Z*F5goJVH> z)>XY_+a$JdZVu`rFr@68=853 z?CR2`Ry9h!fvYtOr zbi`W9W<@b=WnLTT7;tE)jIk?8si~s5SuyXGgiOkZUw#ri(GVFFXg!B z>bS)wXRx5XXrgrSgCkWJvkz}^M}S>^@FzQ`|1uWZahoW)RXwGY+{V-ptMtbi6T(c7#{3!?Gj^A5-^5AAfyY*(tFbRN~ck{AAfMpx9FbO1weGpSO=B zaux&TuaB@9vkjMgU)%9W?g&=Xo01ZJg4jZ+FFRE$-xlyK^%Aa*jyGB_C6~if)aYbJ z#kk-8_dhvam!`Q-Y3>ikY;=rCPCUf~6)3$rmtu;f0^w2B`S@222oE&_rO0qU(=SC} zwiiz2mmm7rJspc~<)Y)VvcJTuBsz<694SV_B6NlNS-me)|5uRHY9&lfS#K^#2 z#)(hrzDW2%{c`Q)CkD&f$fzqj8*TeK0Sgu{KbB~&mOCczUa8NX{8sScGaI&pJb>)9 zeAAd}$xClNaoEfNgTvPH(%)aHPoMmjzv(O!3kuZ3{nQGayyy0Cax6|P#_MVYPM>V8 zz_W$OdlZ=42i` zUhhE8Un`%%_#$gl%0;DJE6?DKJ1!TDPG^ZI;|!f5H=g8G=l{IZf4I|BPJy7vfbB<_ zI1$RvbBf$Zt-PKRrIq~NTd5r!u#FJ=o54j6#%!-*AL~BfF-C-?fi<7;{D$Y3JkPGw z?&+|?&hR`=SUh3J_8RR%*o`j9RIkZL|`{btj(SZ*cmXI{6?ulG8nu zaPTClPLpcCq}mTZ0)Py@7R--gm{nU>kBH7$k=1GbE1qWLb>2dcrP{oKGq!3w;%?5| z?W)c3aSOD+mA&8lH`&%v*@7aFy%`_>$&RT^|7O&fY8E4k znF2FX19P}3s{;{(zw2*hC*mG~Q9gtP{W_Tky%BXkVBwM0w!JoVS_V~YKmj^y<)bg0 z(}wjxg8B)yM}n-oZ$7Z^}{IHzolGsc7LUCh38fof=v))1vf2E&MFT1e3z6`W+F zJJO~uC)7%|mF+$tztBAtW7S`3U(E*ympyCW=x(nKjVGGkIrClP-M@p%5BDVxVuEzW zz|A4?DaGjzZ_p|Qj!-Cq(00-&)^hDpzr=;TT)s)!z0oqEO+6~nq4^&nMt3FJ`AFqk zYeQF%w8p`kRQ>2mh9KgjBDB+{MC{GhZdh|}OKE{G=spr&WAU*g`b84O%l;QCB(qW1d|4=Xd)n6`zzdDo|?LmK-RN7JEf`Ccwj?(C--(!IqoSLKm2r%!%$DEz_dfq7St5t@0Vtv2=f_=YF^}WdF zp4<2BGL|wpsA`bDZ$)F6)%We)eUC=@3-vwS{t0=~?IXLpU165Hy1bt6Tjq@4ZpY>e zdj=;ldxob!9dVKD^1H(q>CIDFBq1;~vPfheaYC!Df)&DXEI7-4pav8_Qh0Mu(F-L} z(R*ct=bs~bp+qZsO;&E*rs%~hGSDW%^1I1hOpeHC_ZsiElF{aPk5J`UyDBh0n3sfxWd19aagGm}(e&X-GJS41^s!lfLC5B-Co+sDwg$k6S>Smys^ z=*nyLv)=K-%%iH5WG)nQ#DH_n(3IX4_4E^QnRIoh%Zvt>9GX>M9_&3tmtxB1kAW^}{i~IEc!Mfr)1_Jt2hZRCRHJSxw$`hYMylhX55O2=Oo+ z@-al41w#KL_|d$8eMk7wrEY7z;KvlJs2IGG8d&7SG`6tF<+b@w+r5e0ng@giFuCP) zQe!+dB~vk#eo5U$@cuKN!KCX>v}9tSNne8MchRM+zuR;EU@AAb)Q+h-5em=sE(&cX zQIGj0>`Rc=8soCx(%c_u(~pQz-YW^pekZsdkz+F`Qap^HV4)pxbN7eXL&R2YIT^{R zhmNH7T+fvmd-ad>1NM)z-}eT@z76n{AdL^f?{rJ(ewu-GkHXMu@emmGT&`dUiIm;s z8CB9pi~v47On+CehoWJ8&87j9fH0~^6rmhnO;Y`AU}{ouEF5_ z{MGEarW;-qQ*^_Dz`hmPs@DIgm3HZ`?C)K-xjoGoDiVQw&mvW za3sW4*2R}%LO>3k-5ffl?BN_bF~|&Ub=GOa!7r%-h)_cA7wW`OBs2wI7`@<&&;{U&>y6CgJ@$i1UM2~mE5H{cXegIc43=@^ zh{ZCoF*Jj7^_)T{6 z=HB?1b*6k|*(qTt)xD4D?#dDr!|MCPx4i;R0$i-Q|L7kF@3LpVU((mNDcfFKnhd+) z6L6#j?}}!h@_rJARa)l`!pW18y&uEu{tuYRzMSlx+K=|&)AP3JrO#C@XMf%)h@tX> znwu@4A^U@%!>5-%rMsV|9@?XCQwdQ|@P<4awMSi~g;gO5yRzH|eL0!6o8pADtb5C~ zCmK8%gpUnAh*3i9(aP4ASzCG$!bQOu)UJVKCy-S7E0sH_N6>hdl-)u$X}%o$&dtMv zIrfbD;6%IqRQ3nnRMXhoFz_&1|9SbwM~YpAZE?#;d-=hQC?$GJ-Aqt@C3f6xl^+K; z+3lIfeV>oY-mmZXBw@?KZYL~G8MD~yJN@zcIobPTHwCQU4r98a;a1x*MNr125&w#g zDWI<-b)WU=@9prNjuf*uFrl;G*FW@N=JrM`TKEt5<#XNP!HFlsPV8fZo|VkE6GQ56$(KNc*gRCV5GGaD)pA?2j0w>Ejx-CgArY$toOu~o4qsciLv zTC2G2Dxqqpb&)Wdzmz6%Y=#a(m+Qv;i7y6U09lwOjuvvo2Bundg@UNi)aHyW+vJc= z)j5{c78FPh-h9H}dFh7H0Y}BFf1sDe^oueLiq5ZS8~!y^KC}(U;6w0iMzHm%32l1EnT(SUT&JY#f|HevPiA%bhqBz;A2zLf5?kBxkR1~} z?AIA5waPzJMKdzSFHd9Wa~A*3J*Gj=9_@jaVzuY0@b$sT%7CXdX27>J1}mN_ zqH4&WUa))DhBx%G;JLr21<$h2&tmwsEUJun*2Ii>Mh66IVn#fp%7|x8%!p@H8S$)% z8S#uNBc4Ok4yVP8c=l6AS?B~4P-j_3WUZ-VN}26!_8%FE4i^8Bz5s1I1#iN&Ddkc8_S+`K9a5WVgD`ondu|^)L zdPqt}y+|xDUIM)m_~Y(ChXhJRLK}n(m=}SPDOv2c1CNL9#oJY1y;ZabYvnM!M+LQo z=w4KG)*cgbT+d}`9-^26eGobH^Gtz`4N0JRI-FDt6r44WFh6Edi@UAgWlZ0g|G(?G zs7L;>JS9v}4qx#TT!$b*Zslj~KPLMN=O~?lm}VEUyR*rCM)tbEtkgjMNYD-(0~4>1 z{a+#CIR;F$yu$8)#|xs-%S4mqtF0XNi6srO{cRTe7R*}Ig!Heb@n{5#1j80SLYn2l zsRRW!ur4{UFa^0kSW0p}(!H&E9!BT4Lpuv>1Q*!`ITRqGJWA&Ua=C2CCSeP1KwnLy zu8ke6R(HP`66H$P`qs+l(ryMFeL$l8G7~jgvHPx8ktyHMqwRiGlt5Q96VWj3uF`hg zp6WYk_vE4v9JGZO=+V0z)jX5(4xxye%!6EH-Kc6r?xE}p=_SNP69b9ispsH}A)tb3 z4Ge`+?tfpZEQsyE;&&ZoVU;R1kn}E)yE>amJ}@v)c~Nh;q$Be^tb@zJvWw~@OLa2M z>THnepfB32jv(gFug-i`ooQD2g`H9X*)PwnKzi>ATzXFZ19Ljhtq_Oz9+8G{1qua= zEEx-lr|y&Z_=2`@n#!PCKDVHCQUQG?H^1P`(r12m1T?n`p1b_$b@eS5XEfddQ?{k&Yi+3xdA2G?%$!>E$C>@c1J*IU2xKP95HjivrIDgbQVQ8 zq52LU`9|Qc7NexO_|K@bzx&K*JqW*uFx;Rh45lylAuwZ`QCD9fT#ww7(SlMfRK=QV z90m14-mOj*GVhb{qn*?X*uRycJ-u{0b3M=px~2C{pfx=Ypk%uR=qR9z1!+QDdChPl zMRs&A<|>x&W8?`ni=BXnVM104Gt71wbhoh)$fs}t08r3Pp=~401S}azFpjrI_wVp3 z+&J)1jfWsskJ<@LxX4|rm@9Wl*QjKGxWHmxA#K3hh*z0dX-9cQNh87ozyCx zAXP9Y6Ql+5=D;eoOs(-0xcyQ;7ay=y?g>TDc3` za@Y|Zr0tkv4=#;^e#)_>9|$MXW5hraJit!Q$Loi3-S=Fq?YMZVAe26xBKV=V!b9pl z@6*532+v~b3Ail@xR&J^fbsqF4&_9Ohmsv@SW+izJM2^SxBP8W!hP#LPYNfqvv6vD z&dXq|+F=-P?^eAfb9Y8W|H$6zkM@&`#FKUU`#y6C?!;S|xz!P=ZA0dt5d=mt>sLY<_vKu?bMq2<7ZrsaJ-XIA z<+wXy_lyvpC%4mn0G);vKaiz6rfWT$pU5X&Wir}~G(#4CZ8%Qwj^;s2JpCvfD`AIz z6cupZBQkfmXqn@mFElMVU=-Hae^Rvda_n4_gJcGM3;St-u?C6`Si=ZgAd_}aaID2f1*DF*$d$dDgb zpF3WkI#Itrt7&QuVF+d@*VMQQf9pyS9=3;W=wlHUGmo>4OC0EFpglTc_?uwLgzPsp zzl(T6WZRK4C@UqqIKw-0n!>P6TGF9^s7-$tGlL|wx9aZ)bCR?77B`0{oBL;QQ>tC> z%!o`XO@;_s{7iUkZ}>nc{Ed7>yC{&hRR@?HOk$6=Sa0>0 zaixJNh$;@tN*T4`2Jam+6e<;rnSNB8zF%{n(D&L;fQdgD965}P5Y>E6$S zi!<%|fvfV2NlP%L6e$jbMv!lh@2F>eQ@#}#+9S)Ua$t#!i&eugcy)tU{WN@sTs&v6 z;m&N%KA=5%5xMA{cJ88Qz$r{}Cu@(M#S?VgR1sx|*UHgujM1n=U;qeBZJ_LkthxbF609ToiVaN@&su;z1K!TYX(ZDGyd@!S3N zSbPHeyBb{b!H~qj52M4nzru#_6(81qYH~b$1vkRlFq;`+>pqi@f_>&=VH5t(_`rMN zpS?fEg_j}OLiC2=WH2?4l74f8R`FZFh0v!0qeoIzB3nqberoaBCy%SriC&IKy z;E&);J1SzNu9G0Q(1z1`lU7m4#Ni%Ot=bS(XOXoH6D8=FlpQJAOm0&5Q)b_1=*)RYkM+>acBL$GC-# z*>6YhaU|S-E9Q(dqpBX0IANDWbozXwOlvMJK)(ga*>&2qI!<4)_Oj3{6aj@McyC$+ zUsEn@P?_)fPXfWI8LB<9h*qIy)|!aZcw^+%MC7Svp&e2cj4$U-MU(#UzhVBtWpLs7 z2RW@`3k_s%15n08!$N=J!A2!-Tuu%uHVR=r!cRO_e=unvbIk02j16Q)6(EwUJ%2)Z z-v8`)U7FfGHTPKrOcGBg9_6~>-6#AGdC5^XEVl+jz;4(s55VTI8@M!rXvNm=oZmk3 zgEa!U->i2X%LJ|p;RS_Le8zaW?{A){&wznG2YAvF)kQgCA^cMjeNM7KT!ksvyVB=i zBnEz(k4=1Qh}(O|LiH zpU7FK*D=A2ll#SQ9954Pd{T-$4xJd>aMB;`tIdeWm0!*^ZO6zsFzXA0NpZ*P;yoAT zW_K2cL-(mkCi5K)a4eqTf!T46{z-8s>M(!o^L!CbjGbr1*ZkKJ-Io(*d*Ddu3Q5@m z-rW+&x(x2xlMYWN7k{n%H?$c@8914UnCn6s$1g3ft7#py`lNOiQC}7Liupt$scQvHb?=dk7*UMox`G{ z|54=>Cj~ug<#ZS%giovZmHHAdUxZ_-s|ucbLO)WUs9j01D!NS-{ns|%H`~y*94mU1 z@0X3ks+>O@)EvBU4yp+ z(B-NGv3v7E-mGh|#OuBWHwRJD>{i4L_%qZsc%Q08EZgoou}Ua+;>m+8>e)XqpY3W_&245>>7P9&&QIz0;5-hxcLDFpr|=IM1Z~Hg zZ-n~r*hAoTQbRF{WVcBQaIHWe4$`o+|5wYSB+Mb&Yo13i7FS2Qle_D{6T>! zMN+hl+6uIhlnh5Hy7*~&=mkbi#7BhsASZ0*?$?F-!1B;`EVbWwc`&DcaDAM2a`s;B zk>8S0S=6LJ{0IIR1w!a4my+T()NI~jhqqA1ujhW$MUHq!Kk0o=EMB~B(jx%yp}wHNH+;$T_E zqHm&bWOSwnfBe=gyojx6Kf?~i$$r|;>inOaEjy(M5kE5t5$+CH%Y#3QQ6f$$O2p4% zl!#M`67jPbCE}D&BJ}#=)X{kz7BNEjsTH>)yMuQU&Q5B}lu%5C01g;kd@bTXKCpbhW#jLCl2GfG3XqjYLZ)$-Vt(3bF{5&MMpQe<#`3)DYAyupW&aZ+!~Mtsb+b&T-h$zrPt|= zy(lZyLf?teR_gR4in=lv>Pkxp9p==lqnp6ep$IHWc&ib5`l;+DR9cWq$H>P9`xcGUZUCkg z^%J3=Qa&`9%D43f-(N>%H+lv_0I>U;;`FnEQ0t?g004n`=P$mKAFm(IY7?^+`+$(a z2x4sh(iy14`E*KHtNXN1VgvZS8Twgae_)M?G)1fLWTZG53r`-d6$2&i;VI#M%ylU7 z;lY`^F=s$D|7Q^D2PeLxAHjr_$yeqe!yB{qf z+n)X1MeddT=y|mtne<+INap+bdy=B{vM0SE9e&|GNvrfw-7ZS+S#Nbn>d(3a37F0# z05V1{5+I`;Rt%Kvvg7~ygibfFn}4#rR;w7JMDNfI z+-uLz2_+M5iUPFf7aD1QkrM@^A_At*DQL82JY54UBU&>MtG)egfmBVz{umWuYcmn* z*-;KyzXsl8{{OD$yFK!cCA zh*I9bzzW(F#pc6I#pY;fC^RZQP(yM4i5@%8yX9>#k!VTWtWXj+z4wRzQ{`{=?U4@1 zI>9Ih?r?w@*nt>Jia zybmPzseZ+AFD|g{t6Vi{6EaS@>? z2S==geyH@kC;3Xh#>m$(&$YtHFE)20H6H<(zTSota2~I7fWy_?M_0uab`c=-I{7FT##XnU1&Z33l??0c`!7raf3hckv2PG&C@03JXPoSf6M%QL*&Dn{!8*9NCNV(lltyeZDY4z!zt(5;kP#b?hNYO1J=ORg@HWO1kTOEkjCAS$f^=`YBJf;D zy7x`<%YTb>Z}f>KN0$h(&u1L=i0fPow(?d{;`g zG=+G^rJyyqz5)9DZ;>qxUnKqQD0-?#ok#ZGLS+9mHxOB?|Md-IWkBs9UGVryz5Y+* z^%nMzu@Qi`I>Vv%kv}c4y zYNecx&fYenCn>d*(-A?C5R~ij97(u91(5npcmb4F52{UpTKy@3)vk0jq`Wn0wQIch zQfv^6^M8|AGQ&tDWuX$hznCh~H&=zV*IeZGdM?_Wl%%7m!KN%Hb^?<0)l|<8AbP(; zzYku86g8N-sNNfSjqxh!R9*sF=?!8=$b-OGR3GWmLzjJ+@opf+h3Tj}`knFoHv5D& z<3u49aY8+H4-oGCBtw{|EMp(*C5zVZ)6ubMM=-!WHeI>nlsjIz6P25#+#KcRDtEea zXDN52a#NL?qFk49la%XFu2Z?m%AFOFvSuqcU%7WEccF3@D|eZ4mn*kOxvQ1CPPrcC zZdC3j<@%IcuG~uHKBnBQ%6&q)Pbv3lJjkm;m948Plen<$qG5Z+bFv;Zj<4U^v;3r0TU(|U;KE~eFQ-F`g%_?CL zX%;PFD&A_`M1b)V9DwcqMgQ^_B2JgD#sL)G82iMzQlbn{2}{+dq4>!@!ML1P<16x= zP}{JouW>KodyI5k-i?=_9~vL<5&JQctYKA3(Y6Wfn2V_r4H-d27Xhi4yICOD9jsjP zxqrnOo3WQSq$8i%m3y;o!tSY)>a=nfK~(k@eSicO*&tyR9b+;XRcOYMc6bKCx5oW1 zTE%{33w?=G6d^*9A+a7)v6>Pjv1|(Age?asE$TgFH~^m)8NZ~Gfyjht_D!DQg~m#= zULI}1s<;J#8v^eb|3XWm9~hwAh(@G~3XQ2&Bxm#n;~vT1xD_{$WlWYw#2Cy2Qz17P zx1(s6;UMU4v)As1yF9@d&39B^*^OUHMvJWK2O7iIbC4i61tPKy#)03{|BDti1XdWv zXft`buk>>p8GO!{SSoj$F^4M35D1raq48@zM!jvub3FGL@q~pQrhBXdd#>zyS2d5Q zJ<&3zIj{?@Cei4m=A*^^19gj@pk#;ZXv5XEWcu6|{=6X2!HH2An3WUXti3!nB{(-V zz6DR8A4JmQk52l)gWN&Vq+64U`vq=x#^0PYsks=f1L>1K@P09&7WgnwA3kZ6G2#qh z{B^Yr%b!VQ2nt_sW-ws)3p*yYc#3UBIc1>uS-_^ds}7AHqp~>Bca!w6GtNup`Mgxt&xdBDqpZM2rjEP690d z-iM6{-Nm%I9D^mJi}n!Fc*#iyl&=-F%ImMqSI>;=YQwiZ;~^;MJ+{E#HfpEDUX`}! zDv(yu_nRjq$T!jJ zFyQW-ART@tI^;24GsQE~^oRzg=#VrO2~_u)3GR*w?lTkWq}KxjWT*-k?40vi?klfT z`RJs*9!JGH-ph@j2=1zR(q3=h>f8Ak9!O}v0fS}&+_WY4Z`M`H6Yt)o57iMXG9AVDn1a3HQXRL}h z9%DE#0<H(2l2W$++D7~4IYN5wkzcz(5d{Jx0C(?70wnn%Fx&?}z66BN%jJWZ-xJ3SD{J+mrbSGsJ2 z?iZvcZ^|C(hAIU6#0x{1zeti$V;0Pd(IGCnaFs2%FnZ!3(M317%4zsP#`{wjk^gx|8EXcNb!S(@~<0L*Buytm|sZQikH7?1ak3go6tPD}Xd#hww9 zFOx^gr{yfdqs@6=#Eb~WMx?yCzxC41jgIEg94MC)XBfst}72lo6UgIV|}3zQRb%W zR`p#tVfT6Ennj|-{6vF|e(?3tQ$jMX z)VnDmMSW#EOh~hQs~ih|P-|=QxpO1Up8_jk&DLAhI%yHB~z%B@sx zxpKEF*QeZd$}LiEi*lb(?o-NrTDjYl`L?;-~Y)yFndy*Z56)<`7Pupf1lhG zXG@}7%x@mQ zW&Aw+eEgo^_Z+_(evSNE`L*-wN*kFE_^ULBlgP;6;NS?3q`y;;=evSO(FJH8-@OPv2 zybg9D`Ag!x6aN?dM)Parw~nyK_`Sey2fs#suM@tVUmX7Dcs{~!8NXzHGx#0iCx6TM z{*L9xl7R}F&p|4GJ6gL}Yd`$|(c@i3_y-O#U*A0%e}j#WJR&(zO`%8R^ENz>J`gxOk1&Ld0NiowACxt zqlrw(RzGlF5xXde>|yzI$DI#rmehh_3E?@MQc~DNV~_oa^(u)fZz(K zS}ET*6yN7r(JO)&)E3*?VrRQ}E%C?Z%1vI4_x77|ZCgikn;er%NcZ2g8yBxDUb||= zy`GZA#oiLnVyl5Ap5=@0Tf6waAFf+nA}w5YY^v=Fl8M-@0EXMh$HkK_wY6MMIj&sQ zxAOjfxA!IRQB>L9)#=V2Ajl>NSYnibB-CELRCQ+sA`v19ix^Eh=}OX)bhq6d2#!lY z+)+_c(NPCSRNT=)#|1}bR9wad9k+3CWE>ow8J9SsGU~wlpQ^5;6VT`P-uvF0_kDBo ztG-otJ@?#m&pG$hxs~ms%J6H2q3|XsZD2AMEkmSOhbo&v=dS1=!$bb5=MI7zL5FD*K|7>k}@9Xg)p(FoBx=64}hNTTty zXfWDX+8T_vM;qftdl-bd(wyw}aI&MjrL;BLRl*yst%~6BgjzgCt6LBoTUI8GXgp-Z zOZZZul(&fkAe5p1C20R^?B>(gR14PWGPc7Aq7s`3pJA}!1gJh9?~ai-19QmyWHicl z0x~SILHVumR#9MEI}9{w5fTVg;I?u4u^UY_tl~>}>qX;x~0D zg6SC)QcWH_vQ(*>NF>VE8_Dh%yD$=tB$D7yY;CliEpC}cMZPbeRiu)ZaI(LD7?i%N z{}e_{o|Nd0#iH?~5whW4gnIUzrr=GnFOzLqNdyeS!1%$h44@SV*toCUI0IQ;L7TXmCzLT|lgrFT+d8`w z9kF2Ch?uO-Of&Ig0Ju(Kh~R|;D&{eaHIR(PS0X*yfyAS_X|*%VtbX|phSs@~?I!#h zUY2m!_f5GeqjJ8rDO16u~QC-WE< z?f6V62Y%|PK*TUY3DPlIP^%>o?d(n(03e3)6Hqi6?>5?koe3hfdb6!TH7J{n#=)^s zgy7zYdPgBHDS^V{h5(0dk}hZaip!Xq`3I+OoLrHl$ zIPiK~yW@m&tlt!VW6?w++yX`uY$I)mbPlWR)C3Fp6znwD5!(?=V1&>ULk74}bfpo3 z$ZtKp8*@{?U~w%3S#?h<<}|$ASZ2f{MyDwB>x(}`Kl$PxmQ1IZXn!|i(hY5yP2)MF zeD|{Rx;w2gubWkF(lXIA{bScb&-X&7OUrNG09(!ea#JtsMNE24S4t0{>J%E{VJKdh z+C&HV?=rdzTCz9J+Paig()vxoXX)Q*`qLJ)Px_PHkF9UmZKUgCL(AH_7IVPTnK{Ji zI&*sewm$fu($9mPL?|oS7U&_-6*SHL!J-S+Tp(S+m4lS@Fbt0IFeo|F4IxarP$EtL zZZ{%EoH!U(?&+ZTR7*8gUKT<)0kza%jh>DmlxpfRzrtL*_kfv^PEPYia*XD6o!_Dp z0%|b*)pvlKgkpIWj6j(P3ye-9n6T;7%IBC0bMHaj30&(DY`PTd z8M2=4tbJU31u1MPwzM3~Cx~@}cGjWIBM?6g0Mq4diXLctkXV^$w77Sok;MLjh4l?h z73Gz48y1vTRueD?CJrN5aRx1W+MNNXfU7%#DQWGX-k=_aj4fFe%oLQr9vAH|xHjOt zBHE*sM^^f^?g&j#$Zz3|a7Ne855Rx_QieB!;O)35l*P*X_dM(VTE=i6flq&2#%%wh z%+$H5ULERCy%?@uTpMxKo9f8HYc7U1(%0B_v=#dVX}AhADolM|M_hn*YY;mS_cmjKk8R@&4Ol89ty=xJ>d?d6R)&+=3RIu7zrK8rSq1~6GJZov zry#eb=tuYs6`fVoEw&W-P^P8mIQ$AlQyAc9wvnvGD-Rn@iiEUEaDhm7tUd`eFAjDt zGZSkf;ba(w1=0a+*)tsYBc1ZQ#!2aP5&jx+-2__5LTuN!X<`G?Gm&mTvn(`rulWm= z4|{&(lb*g64H50zA&s6%_W%huHU=6O&Z}%JZR|sx$tXxV$n;c&wDRfvB~_Ol2q@o+ zu}wqlL7#bu>+noAm{Ke{(gE#fU+Ni2v>!gCy~8xZEDy({kuFFXnAqZBQ@k`cds(IsGIT7ey@HEq4JF8GG_Y|< zHHjIEKr<(UV%%thwiz0f0s}#+;KAih$!dhC$y6O96a-&`{tEq^MO&z2Cs3)NEpJ=m z(PdCWW8oOIQ4=m(V=;PQsIe>+R)^6TDIEmf3UjAo+Dpj%jPY1yf??mhJ*=_$%+IoO_)+}MR=LUV#TKXu3!^Z9(nRTo#6QBeb%1y<8aX3<5R|91qTgCx{ z^t^?&CYgPQ{uqzaHjMW9rYf1PMnX?W>5BBfy@syeR5w9$8^;FOhT3||n$Cv0O#={V z3+65@oTTkr+E&bh-?IOczG;D$Vz)Lz2J6H9`?Y?U;EOQ`rW8EbeRsnqxZJ=N+q75) z*)D8ydzfD6=_J4A;v$}xhVOKFWELjG+hgCB>c?KVC;;Yj@Rs9X{cV}Cl08aiTfJW3 zrfL4dhNk(mnrh}%RWGTo$DX^Of;kp&=(y0uA@sjgeGq;b${vFk*`{upToJV<1=}vBcUXmKd!X}HBrEJt-)R`JO1p(cDV~q*d#b>}jeqxE z=l&tILQM?9U!=8>+5?#VNUMv66VZspPJTpY7z3M}(o*)URR4qUW1_~C1rb7(y(c^p z{nGx|elE73k+e9t%s6OxNVngq9HPf-R>q%^rdDWvQ|N!=%rhNHe+85ID9VeO@#WVCx`X z%PmttwJBE@lL)P#^0R4W2+A7TzFIF+%c=05}POR0kty<74s^{y;^PrU{E>*mwTnT_$m zwx~WlZ%mXk+i_W=Kw~4V{z=Z${%bejA-Vhj9&9<0w!R?G`nS0#WrfsuV~`c5nHgNe z&NAcqkygB7qW2_pLl(VQ+*lrtb|;AWl5sC)u251uHUge;sHK6hiMLZY7$4RmJ(jI+ zC~v54I-$INE?MpuESxv5X5Q@nHkwmyq`{w93H}fT7{nZCymo(AqO3jMgZqQ+2-YJ2 z4nrf}6hIX4s9T8RAw`H)TsAMqtvx>^4s$rF%N2pggT+KG>@uaiqXg6xXAXF9tOkLFYnAoo9uVWX*lhNugXZY#=~Yu>Fo`` zXTQ7~0uSEf+wc&sh%WX5KGJ8I(U#3E&q5DN@7t%EG;ITdH4f9FHRL>|4&_O17>Ge) zgZK+`SvkYuVlKy(7TUlfI%(HPy8zoLm$vCNDI$nB6Kt2!h4&NbCQNFkJJSEU5Y$8p z1eJ{&pyC8=jUEOp6zJbglbr5DOl$7@?88*v4ypOD_)ogMc}#`rf3O?_C7zbfOAD`r z-5XcgRpm^O>`&wcLAojoP^4ADhrysNp|x-7SMP(7)PBZ>`BwBn>vkPt!fU#neh7S~ zxd0Rh9|aHx_Lp|}Ui(5@S2Cr(k}+ptvdwxKVp*04k12NTq2=iIYX(b}{03^}>{&4Up4om)Ao>ZrTDHn)mNK=l?tT|H}T~i2=|SsL%`<(47lR#xtNM z{O=spKN-~K+H_<%olX}ngv=}}{{QH=7krN_SGJA38PDVsNq#jH59mKlc#lc@#6~h) z7^c%7^1oM}nXWTK3^J%=hwTUG&gQ_@-M|yFz=*9fl$V}k-NOa-zz3;S(u?Bwb`!WO z<)zB@uOKpg(GK+gAY?LGXpGb^dfh>HbWd@vl`y11%C$c9n&Sib-zUTbt$>%jsbEutO(XsDZcxSmUwzHB8?L+l$2Z(~(@!?u{L@=D-Fn;YoA0>uuDkEK_r5Ln zKk&1y4?gtshaY+Lv2Bn4;)(6Q{MD0BJ-y?ZXPY`r3hhQr_F9{+D2;+mhv~6R zf2>*J&}CpAG_n5fw?16-7}Mx8hMbl~)B-R3%kb6;)N; zs>jW_dAHye-I7~&D{j^8c6&Sk(Sz3xW&07v$m{27V~&6PFe z6!KD$jVhQuV6P#2hACtTd%##bTrL;}J_n{9Z4qMH!`SV9nrb(_MXR>e6O0;hSP$^d z*2Fptf96B;$#{c*3M&2SSnE9;gD>lmovsz_+jC59vh;(HP6PI{U5=#$gzqXtIvGm^ z5DrylCt#qy?}Y3i@$veM3T8OsKO!caeSny3IR6u{(HIG478ix@ga2LtPw>(|3p)<+ zcEn^;u+N3`<9e#~e&<-kG~QCgq<{Rgj*oCa<6?19_&yvwmcoHm()u05f8cImIZu!U z1Xf&P73qa>Z@Y8`aSR|Bw>SviulisIqbwNN;Ap|ZJD$Ar*lDy*n(0%t6wU`OiQZy} ziKg-aIN7A^XVM^w@thvg;Ttg5z{ADi%*@Qma%Q`7a&imvj>sQhFm~8s!^e(r6*><) zY*g-8$GFVHGsZh7WKVJ&IhJ)EmpLP&B)``Rb8Aay1{zOkdhoq9mtJ?CBRea*Fu!iZVc83FGF%h$a-BKOY$xpj3tXAb{4B@tF_~WH$n31l z!kk6f8QEio)wwF2B~FKHWY&lRuWQmV!?J4GF4t+t^gf%pdW&;H*6M#aPs$#XKQ4E4 z!RUh1vhuPfWG%@#E^}7i3|E26;pFpYxF%%fJA3a&){*74t`cW52cLI!YHCi-^sLnf zMjoCsb7YB=Ej(&O?^@UDYscr0xnQYt?08pU@7~OrnZE3d;fLq;-l-%Ddf%8(klA}6 zv-hFQqJkgq$b0p;F>5Y$s=2EgNB2IQ)BA8hcI9Qcb7tifWF_+_JD0jn%I!V(@QHb2 zb7$rCUYvE;rh;)U{$|&zw~x&($jt1$b>yn9!k*&kS+%a-OI^Lca30|tF^tJ_I55}@ zP;o|{Ge4uyG17Hd=BUF)XN<|1;GCE_$yMYy&ABXNTgEFHuNAzW`$oo_8E-pwXMULR z3ESuT%<*?e!Bn4r-u%mNy6Gob*}01DU-bFTS6rhHU$ppf`K6b4A9le-m)$g&BInHy z8A}@<`uUpkZp>BJU9mYk&*zUeee&tjmg|2!wRqu;8*jd4)9st@d34)TS@{KHCV2zZ zC*F4Z3omZW9zS8qF;DMsu}2?sY_aI}&N;reu72SnqOImuqitED=d5!s-gM`ETek0< z88~&yk20OWz2S1qEbU!A$;pqH=qk#coOxVkm23F)-aE63Tt%+p94X(?yKa@5J2o$; zcZ0{-nv=_o%{~gwtb8@rG*r~39T*c|loRIA*$gZnV#bIK0)~ekx!w7J^66Q=%)G1Rqt=Vc8W z<<7}dCOE+;RyEfbI94%pa_T2}R%Or1o8+8bqdJG@in67Bg---U;FW>)t}!wY}Jt`u3CGMYgy(nXKr@k zRn5I$<|T3><7V|fx;Xc+-pf|acAi%;V$6AU$6fGx@0-)+X6Csv)@;Fp}IN*zCS2jRI@_9vW_2$2biYO&JXrAiKVeqh0uX zCbj)T+M_abvA8H43Rjd*?XkGN3!gR&^gr#WybS|RwVcJCHG{SD?U-~?1|MIbvM1x^ zVmf|@o`InEALN5j4X2WoTu)svo-pu|-b8VFy)yVa*e~|OIPy3@bzF2Iln(RUX#d+7 z)8`9kX_}HzgM8M>jcf2bmAc=MFGIv2uS|O`>B|$?6BFdj1NH6X`fcLn0>oRn3})*D z!qPw<8Cd8Yw8rBs{SkxnNT;Wx;dRKXOXY1u+J7|AH(IuE7C!gjyAXo{A^E>BT(aB2 zEU2%lU-iz_r$7DZ+Oh}cU3~TCaqc6io55#M7f0jE8tL3piFIzN#5{hCEeaet8^hRr zmBd<0Qs)awx)RG+tsWcJ$(U3Gs>>HeB(#GCFX*FiueM8bH@!KJ3l?2G&QgE34)|l}P#?T7y+?1Ja29mc~)H!78^AX&R-O9T)Xb zd%KXfi_)w)5-9CqDZ^Y>sWg$&UO-yfTuPKi-;LOTPEzF*N@L10m|jGssj>Cqnf%#H zTN3a-;M86K)qsi z16Diu@okFHphJS+4(HSNDvd5CReXD57t(6e()J>) z7im>#X=CvXkshSokd~$(Z70&Sw6sM?iy=))OY1>e2ht7|{`D=B|M$OTa#u!OG?8-Q zv3;t^>!yMZ7}VjEFBtCUOf{SUt@}I7@+WqOld00oW~Rz$gOf5m-|FCY3`fqYjM`{0 zR0)UOXnZmAdd33t^94q{k`4@4_n<_Asf06A(y*#gQjfE~Bf7$RhSYUh=Wy+3&VfTU zm7QTYg)D&cI#WT<@aS2VjK=ITyD80jFw3+%;rnAfV~9AVhUZ9VV2m4_4KUgzIst|~ z6y|ZJDh$u0WNXJjxpS%A{>O#&)eA(yIvcem135Hi_+`#9f-#1vc2YbmP7YAR9aWHj zNuv_Jh+(MV_0};3rra!3PQEz!HmKA!Zo>y0^baC4dq*CWFB_b z8o}iUp_-ZLs3s%~odQOt)~W{%pV)rLinl$EX3!f1j%{G8|P)DvcG&EV^rrt;9Mu@4B#}=g?h( zP7%ord^U+x&;7?)DsoXnz1c$+rVoKVe6oW&U~flqDR+=o;!O$no;sV&?IIz$5o zQ$;DsCOX}Z??o6mC(}d;@U5o_gO4&?!pAdcJJJ?z2TkE)0CWnv35xb|6N+&Eu|Yd4 z?HCoEmTGE8`>9fgD&l6$TmaO?cn0p944m{Nw^V$Hh{lSW1`F0USbagWnSeCb)I^Z9 z(rftsC%`oh5LEZMOL+V?&NiQ(81$^@*HYk6V>%?v&z7nqdCS!otGrsIWypb83#(7da4Xpcp zbG@y^Igu`MsxME_a-}=w)HME6UyG9@5P2cy3};QE%4q3sZ#Uv~;nvKB5o@gs z@Om^uoiYZ=r0>jLh1OEAMB}rdDPSGgt**&zu;<2R!G}^_{|D2W1J+)`27&#c%(RWz z&lH!XeM2BU-M+62S#40=^nh*2fpl%1?&%xJ_m6*~Dm^{D&cE*e`!PU!Z?b5R4?C`2 zdZJ*b(_C?fNXOSuA#)gJNgcjLi0$DXs{*5uV}BQ&Q0wXHj3o3aIP&OC;8R;&!9+<{ zxHTS4MB9=j;EUd1qN{W{KZS+z7H)%=_af_STKW{el$+uome=1DUMpFrS5se?GE0aF(W(R-is*`V{la8~T(J>>f)O)tjFYqVHT;I(vKyOL9%00*)yP zQ~cUY8+5xrf&u3SZFhWs>Kj6-Z{Xk)Q=K@4s?AIWnnD^Wie4OoxK@)Tky zHTU4LrNeR%@YePqQA#>OG*XgMixN}VevBxDkMGAwth)tf@VUmy209h2Pf=u#+@ipV zL27Mn<3nvz@V%BudpAz25&{p6k6aq=!2=pdHAg*wN`GB>RTa*+ELoDe`PcJ52R_I! zM|Q6noX$0JU-S9GIN>4T8DWKZp}1bGRL@rb;GXFD&{O1H<$K20rQfG7@IUR(3~Ude z5{rL@f#`bfZO+Tr^KbBNLZi4!zCylPenj3Oza+bq(aKS9dpSW#Dr=N0l$({um9W~Q zu2rv7vpgkmXF1+8-;?n4c(S~QduzO1-pjlXdVlNvy*I;`?OUurq`RmeFrG3*k8^Kw zi?!w21=?fUYuX;|Gc8k}p=)}Leu{p&zFL1&|Be2RzDNH=|3W{Yd;Q1zTl`V~3O`kz zf%@CHDzR2<5F5pMxm&(X{g(b)#+ZSU9Ii+-xBy^fT^?I*`LH+&ArM^k>8ci z@T~S+?77>s&+G6N_{R9^eG(m(Z8nF`Ea%tqkMTd2=BSscGdz9%eSRj8Lommz^FXqE zmVAl&8}&5z3io7Bv4=P-85jQouc5!X3p@eOsh+Us9M8R;-+BJ#`MamV+vDBr{f+k> zZ?zUJu|vr^%W(C z-s1ko9RY`_N`4XF%4Y}#!c4&<)Cl#$nZotLEy7#E9^qr*5%DmoNUD^ck=~O2DxU+I z8s(kfJ<2;gW8kY%i1pOb4}J;^cDKe`tz8_DSp*|nt#3jX8()+AmMZinuMlq#?-RF)zZ9PrFP7Fxw@Ob* zzn0#R{w#edeIpgfrLte1Bgf@l`7-&Z@;&la`M2`Na)B~RITEg2ekG#xC{HRg)!Cr& zHR`qME$U|Vef2Z-ALR$zz?f6axP?60Y&CP%d+iGqD_c$o^ z1MXXH4F4ei8o!rcDkO!gg$|76HF1n|1UP$xbh5NeS}k2ET`O&pCc1TZxBD75@(uEb@?Loi#^2<=#QmuIW6bYd&l#S(JWqOF^NjGa-c!Au-aEYyd%yJ# z^BwOy+4mES_ha9FUj^oWop!VKzV9dIvT(gih-<|OaJ_4gx}@8sc6p9+iTfGPgXX&7(8F2!jee$&VI<&c8~3F6p}0>h z1TK!2&Xg{eu90q(?v);sM#*x5 zabXo`^*Z5J;eO$9;W^=T;SX3%e-{csxzoiZVvD$6TqFM&)c7ta(WMMmroz3jM!83M zLmA~h8m@$wx%Yb(V174wAMn2C&GUJD&A#=%pZZ?)ed+r^yG7sRzt>+Cm>2jc@HMTm zG6%E9go#$!aa;p;DwpKeb2o9@xaYCT-r+vszTq19ZvG7Z2L5J#A3sqj#adr2+#&o5 zEB%1r5|0LCw4%mIKB+=lEHz4JOXo_%xu?M?Q+4lgf8_q$ z{k6N?Q-d|U*fY#K#yin_v^U>(m~VpbDBo$mxNoJe*Y|Vq>K(opedF~7`d%HJE9PL< z0;l%~FN)LQD%vC!D>IdVvRG+XZc`pno>fj#f33a^NtEHvcNe*5x@W+}bDrlU&sCmV zJ@nVMT`(3-#}c4>^BtrtQjs`}ab z#rk^S=z0B1JtI5N?uIP=DgOZfF!r`@y8bEoG`Pp0=+aLXF60UmOl_g3${-bcM3dcOoMj{&zT_s#b$ z^Y!?C2YxbHtI%4tO5i<2GJ2zf=|r?!Duxs8@$y2sQGP@DgYtzUxy#)ry1(##?Oo?P zT{}m+M!QYhf|dP}_B!|wqmR^!bOD;hQoRGTcCLOEB+_Pmtp7xRkN;}_E&d(;&;2au z$_OL^S3xeKlPwm_p2ywG8$wh#2dnmK@z-LJR4c8=`n(JD{a3k9-Upf%6b*E?R5?qz zM7c@13$#|LzT+9@YxJGzyVdtIP|wRg&hPV|K>TPs{+|e{CusT9t&dO4$TT-lF_M*`@qN`AWG-eNNpAU1zep z+P&8^!MD#hQ(LO70atrKdq-QNpB(54+!Xi~=|DU24i9qn67DJPeeN$@0Y8#&;Uie_ z=kwq2!vw$3B3vLmDEwB~Eqoz-Eld=T5>FSe6Q2}660@XZq}L!RXUYM&S&l$HwkREt z3rXb+rB^v$S*u*BY`~~DDR%<4hm>u~uR#AVDX%GSE4!6HDIY0+!>Ze_xYRtgP#p{U zn4%V|rErk;s%7ddwN_oAE>TZaL+WX2OkJ*?sjgNpgzmLoy|;EGY-YTsqP zn|$~Ap7!nXeFhBe_Z4arv>x#OyS1%YVFmgTpbQ=w=ehb?eWQMxzEgig|5nfR=llQc z&kc+UaDmeUD#8@ZohCU-$z|A=1$-SJs|7yn0o zlrT{^2Cn9sFjrV4bPC;AeHUP^?-70`JR&?HJT1H+{6=^iy#GVtQ{hWtzmO#k7bk$z z7K^2DLazX~Z4u+*kD$Y>6R&~f|EaiHyjR>RJ}Pb(cYw#e3NH71agX>Hu}}O`{8r46 za-`wXXla7PO2yU)GPo#QF%54l2qBG2+gd@v5N06>4R$B=*FIsN zz=(B_wr;6Rib*|EFYvGqSlA^^#5&aEGeN(*ls(EE^)>e{Q%;QbO!O3aW?+X?<*D@) zdpXEow=af~GTIF7VJ!z7YM1_zez;%qm-%b`OZ*{!4BTanf1UpZ|0e$y|2F@#{%wI5 zFhVk{#4tXI)1bMpgEZd2-N0>z=DP(_`6=!Nt`_6jhWXkoR6*z4gLOC&+V%|0sRoF* zNDoWfr5yQixkxSsydk+0^7kX~B9~H#9U8vtt&{=wI;^ZRtxjXu43mGmg$^MmY!jaX z{fw5V#ZD;+{izSyUyfXe_O{5|9FpD2y*W?Nm21WMr=_i&Hz@#giIiZ zVOngl)k>9rwbr+#R*S_zQ4%geyaHB1(Hif27?F6x07AasZ|!qtCKoS#FW>up&-2d% zbI#sp?c3UGueJ7C>+Dl{)iPHXm&=uo|E;YqR~@eW^UCjc|1{%quR-g2xt{9&(%?FG z;HANnr{8jW&aBxpZ=QX_tvNT|aNBJ&gE>FCDQ9-*wwzmT%PGEOQqHY2r`~-o zF}Tl`d#{@p-Xp84WF@aIVh_k$aC{d$F0XP5*=6m5$qS3?YI)~!W&S12MLfEi(p=L~ zziWG%t1OEd_Fr?FYZdan52m>$J9#PBl@F!4a*N96Q9ZlL%7&a$K(RVY8>*5WKlZ&AAvst}4_xkH%rN~;Q?>D`u|D^sx)*l(4ZPwHWa~fT))V7XIR@0(- zvaQI1d^3_UCr%_d;*ppkZjgZE@4Q6Wt z@65$npZqn=6+Fd^j4^9kgD05QLAB=0G?)4Vei9>l5DB)Xevz76t8=~Tj?~=Q%%^nb zVAJYqS_?pQn@!-;jAk7wNpr31jmN68Tbj%0!QP-tEQvcW2_#=-oWVyR!6{cv0)dyw zXhFJ>>oD2|Gw%(vXope#J2v8e)mXTKH_^T>v*=aR82hGay<@stjfM9~C+ZU;FHDY{ z9L872Uyl4dG{=#jtw-*Q&r~CUj0B~5nXpH`j)nB7{TwwG$9!}fLpKwn#&|I5>p1GN ztM#zEB!+Eum0?G-a*?I}{HfbU0(!@afC-aGC{(V~Z6w%hkr&?-jS-Ll<{vF^Sz2$H zc@2V*zp!ua?Z(2#c@rIVqFK~}`EOY}fN876{Q6zL)o4`yUdFH|c!AmQeivZw>W;LL z2@qc$e@m^;RA2O^4TgTIjt9~NL#0-;I++OvLoB2*bg|GyAQB{+Ca2KFIVp4j4EcmK z-WlhR#!VSEj?#;Pqs99i9OVZrRs|f{gz}cBEso5(X}@`5)*H*SdutAc^-}doE~wG8 zUNN$}8QGU)8`)Rn7xrRN+b+TX{mWvq) zd#GSB(=@Wfp2t|UH)z1f#bYBEq-*4&Z>+wF&rtn#xrROUymkc3M&AfV;cw)HJ*ojOjS?rlbswp$0lRdN4y=5-fx;_9XDX1?g*oFy= zU_i^vs?Ed&8Jqp*yTUicce!u!lq)XhLNiOPEq-g4-+H+;a#OY$o#83ikd0~r*82hL zBeUp>(1r276%`f3j6qt_GTob)n`>6Jx^mE;UpAPpejKo3;0&tlaBHh+{mXP8^;@BA zW8n#C+HZa4&wImI*bDcikrJ2Ry<0>=fbb_`$kp>bx$e+(V-4^=%57S)VAiNKqvj9j zgc-rAs4Oy!n%^KbDs6U+8JR)CD#bD=$tE{0$=2L>GU>!yYJC)l+=Nv~u6&xB4qCAZ zwkdvEg)yj@b&~slrPZ7R0U4jA#sR1lnKrY&fJy61Qm|K@;Z)$al*&Ou5QOm?;ct$% zw#EjbiXt=)mVrh;wp*SU(V z&sC2&Y2BHg7u(NIzh@FZLHn^v zXCM)<4hF3E{MK$VzPFFpD9?9?ZX(JHN4YV18s)#VQSKF#bGm`@29ub8)Dg}j0?HFv~rG;7F(;3*(1m#VtMjTP0XZjg@o z>NHDiP&Xn?);7P<)q6?6b6&04&3HQC8Bt-G8R%`kWr7T6WkPhFUkXUe%oeDCXP6BZ z@C+3=Xf}55e2wEX(*QNhS3A)l)X)l_H8BfNxwv)*MP$bUBoJ6+#C7;__x*wW(BN92)O_@03;w&4Q2@_Ojzorc`e4mo>(_#Q{qYFZN@Nm;6QH^22x>;cYXG;1k{ zc-<*nZhhvs!R;H>=NL-C&LqaaM4AjYYyqm#)$?1HTtF|m&HnGhdgBDW)M_uWSDJN+ zEHvwq?(CzvwHx_GzyeyJ|5R_DCF%xPFBXDny{Uftp*;oe^V>|p!hxNvzo{Mg?;%zW z)|*cjl^Y)n;g~8P)fXqFtiMl?XtVx)cO2(r2GsHDBks_T9I1!-E6_~17S%g)FSGy* zxSk9cGOr^8ULLUaB^mJf>JF#nQcK0pGX_-#p|JGHH3i)Ot7qFy%;t*VS5m)}2M`!>1#a)c%~QTDy+*b`*Mx8wK!KD||=J$v8MtmY%phTcvp`k$k>?oP{{ z(%ZE{Z__%`+fEETNpH_1_5JjQW!!<-esrEgZ1??C6Wcu0l}c-^|8OS&os1ZH18+&;kLwjDDw_3;cjlf$>3Xs7ctzhG8C} zL@9KK@@Z)Rn=-+5Cqhs_v*{Y6y}{Jlnt-{C8t$NC+`(XZug8j5XCOnpK2y)&ngMNR zunuXN!S?~x=%OMv#by_iZFW(XWEZvAPTK0)6tD|X*8rx~wK7~`1QBJ@(bf{mnysrr zRWqTg>AnVUs>Jp@ZT5BC@0(`i5@^5^WYYm5s^yf7j3&-+4+V^~r zT0ZJ}l@S_#ia^^h3N=0)nJ=0@5J0;IIwp zlH(ZurqcojToqDZ^irWLWA$P*DT{YQXCFOFch<|!7OHM`XElGS{v@@(W6fy#5mb+< z?CYgQ*U*$6i1ze;XUC_YLiWt=`1KeEJ6l7e`%cu zNqgAi>O*(WHEJh&Jlc2~qp`N?P-{1rYwo}R2JW%8H>j?jK|SlWWzx5f^c%m?+72lA z_U4|^pn7_Yo=qS_8+HuKoz}C`R|lh&%eO9#1h(1zqugRw}Kfd+FE#v(l)%m!%H$WAk|F9K)3Ok@N0 z4N$>kvj9#q2c#%GhO!}Fov++d)AUU*rq)IL*!+tGS$&}^*tK#2);P4`1&|nFvF}Fa zWU9x3S4XKqkGxRfV=Iv-ZP03RIZaJ89l3&=z^3!lX0IoIg#hZ5O<$j2)02}m?*^ML zK{5~-XB!_zBc;wsLbRwMNa)Uqxx(&C%AlP68pu(d;s6IvG&u^@yUi$au8X~e9HKb6 zrIYmuMgVjyfL8(d$WFVx8)rcl5nK$Nk0>d47A9Mv;JF>iq~Q4pQs~P7bxjhgS=S!B z1XY9$01jJqO}uYkJL{9~I z@((*vWZZQ)DKU_=+yq8N}Buo%QFRKpZ>y1Mnr~r9Zn~O7zFEPgH%fofslr zfdH(qw!UX~HL|H4PRTK{ugf>`R(Mtdiq`XGa$V}Flq-$fSkCyl0#3-CM))jnMyOxV zL+S%jFLV(wcuen7G0&?%9sriB*GmFArb~dAd+JeFAQJXGhbuWpj=Hdzfb*eogzgBS zfn3^o^VQ5=95iIs=+}OQyPDR}2r#w5Ww=MNa$g;1Tls#AYEZb`ZAM3r0IxQU;`u-n zWYXe{6)b4I0kd}Nu2LugebnA8dnuGc?7xqMZa7#DewCXx`$`Tl|65jfl3zXI)B>vx zR`tNL}azGn3cBb z55N+6mLvdN0T2Bi6>+GQa9~WP_sPOeIw~jEM-Sk!NHNV*S6^1|hv1_TfZZihK zTJW6z>-SA-huIM8VcycfVgU;VdU`6%29;)h{gK(QH_iOX#%ahjo=(4_Vyso1elG5= zm%H;zquy`)RR>#}zJgM@ZSG$nI;PD(-$Wl-d|jg1uir~n)Bxn3icX*wQ`h3Ju0P6d zR3GeqcUEOX{@B_;dZEAdxp+T-wp`O5^rC2SdeK^rH2_zrOc7aNAeylp)rukoKUl!p zp?(HppmZq*joAMEj`Z*cZ2t9>ej7bx{8H=#nilTlJ;=bJ1u9mm>dWt*-AO-@*`0{X z51n1epNKmU@nsS-*2dU%JKQc2ONpO?`V?gZx$X9Eg{9U@rL+^n#rKLCnF)1bZjM>> zRp^*N(adaP(Mu$jqOWf66^PuN9Vq%}&WzYB;htAyG6zf~9}uoO>IMFy%|5v)F~)8- zBSvEDMk>!&`Q5pBGs4`PVRMsjtof|!UBsxhn%!Y(K&x9P$cCt3AKEb%cek4{-Fr<7 z2FZA!DDF4Ly=_(>c89V{X|4MLH7!Z=)pm{d2*8bK?N*by<8Z%?9-GF5Pfcs)0RPr* z?4aMeJ~Q@v+@QxJm|^#LOQOeH+V^-w0D9z|nX==&0K1hDc2)5+3J$1Ckzd_%YjEUf zh04~lMk73&gK21j(NRpyerrNzyg;39gLV^8_&u+vhoh=nynn&An44(R&_39$u}__V zogS^yg>7x8)@Sh1sp~hBZ(+~c%R{~iU#@ylvJ%s+^|h&t0D)))-gDJO&D75zQ{16} zWa+}Vp>x@8GiK0jmSt9taRqxj<{0rTOr^pV&02{)sXA8c6hc4n4%0adW8@2(ctnn0 zGQf1dAwKe=24m4ypoAUpQnobir+`2LyJmCAWX~no^;~E?J=s%Q0i9$NA_4rkB2;8P zG&O3LXWIR%9iNFJ7uJr4Ex*_^s=^wd#f_sizB>*0u^}W9I1jIcA^dVbG-1D(VEC)0 z&9KtQ%?aIpUe|=gZHgP}79>+VyX8o#%ab52Y9bPPJiF(dZ$`0mfO{bF7KehdM$?an zKVg8`<&sQL3aRgP>i4!e3&9?-C{WNi0rrkaH|Qls_~%H2{=B%j)?(&c&7v1H{S_Ny zptcMpn=HonFaOn;p(5rgoL;NHmvh#@-CzeYXpo|%ssDmVh`I7 zIkCGnzNbh~1XrLV5JSx%t*I1-*Jy8ZG;0$aUh2FT9f8<}KS0+ef~^+9T;wW(OkqfylgUe`JKmAGyYptv*ZV zH2CV8Q5VewUmz{`sYL7`t80-5VNWHlb;sbxQ2@fCikLNfjPRd&!omZq(ki&CVDPyK z;;(VsKv(Pn7B^W|(kvTCIVIZqh7@ z8`U(G#ZE=NBJh3BQtX@2nHklshmGomL}1a|#^PGR`L)U3~8(pUF@w8&WU!TKI&5ly6F zPdzHKIgQrqsa7(Ra@foGl_v-R{So0it>(z}GOfY6VCq;^cSEb1)!v4{{vx2t@Q85Q7Up!kn7!Pa#o4=frO0 zrLE~ML5Dy-+lpNe;)XtCtmz6B>+DhA7>hna*-=N0aO_BH>!?TSnTxTKZb=y=_(D=h^eL_1QxswTTa>RK z=AZOHgUg7Y*u;S09|mEIeb1!M#uZYnm<2p@;t?tXSTe+&%tifP*3WGm@d3A>>4x~) zqT_L**8(4beN79J{YdDO`0dGf7PERwTIE5($6*X8P5|QLpy0#FB0jd5MTdcp zk;KQ9Y`EyKX?=<^UzpL`F#e;s{j~C+;Dh)%0(``Pk0Tl%(&53<;jmshSW;!|I3`vG z5KZ5L?qirkLAmQCEGrNlaW<_8Tv{mVsxbC*BNO(lB;eXV|4IJNG%aO*jS#8r(1^#F z4TsW9_eNri#ncJANtG+~@&qA2#-i9y=+L(SEX`gs>U0U>)j>0jcKK?Q5aoJ7Jc_N5 zxU%3(tg|_0bl|3dGple`=Np5(GMaj6K?lGwpiHk;dtG3(To0x-j!RN!Vo2DIZ5m*< zHiD8V@V9VXVaLs)uF#)M5R>#9cv*=Y;m}GQb&DWij5h_rUZe%hKkm*9-EbYBCIvtp zN`s@5YNi7DMmRsi3RYb?m;U0Q_*fUS^?Y${r@&eIY+)H|gcg`DDY^*sXs-w`O?-_F3f_hfn!n@YkB;+7?ER2-PWo`Ei z65oAh3B;52Sv(u%?{J$~w!Z3*$M~(6j5YO@hu)cUY@q0~;IYPy(>oZ@&$6bBh_g5Zjp6vkkCg6Jl*sY1%YJ{oqHulrV?@$}Z>qL$Dx z1^eQ|bUd4{3W`{3Yq8bRppMCM?S_H3>f=_Jczso`xZXm?i>)JYUTiImR&-%dUtZj7 zh^LuRc#4&Ig!5x_|57Ama=HNFX6q&t6e}54|23Y%4Zt1PEr=-IzfHVDg*9}Z*}Am? zOP71#3h3=MzXxn-(N{(oHf(4UAvZLM%U!^Arm<$P-})+km$jqv-9dq(1Hs;SbgSEs zMGhPVtav=LU>_g?))&T_ZA4?gK+zY$e!y`T^vPG<1pK@m?*Yd}{5+_~fNS`$YjO82 zHX^?u#*YQ0AoA^aw}y9n##_$pT=BvR}IJg(Y8BwMkD*fCYxh~%mdiQ}r51jSao0*qHz_6eM^@h6k~ zD-fAMD-U#9hoa1=O{~$OV`QHSvytkzQ;Vj0V9bm%0Bxz3sGxSPvBq@wQR~yM|Fw=g zO&0iIN3uX5IxC&*oP`6CN2Gwy8UZ)S{CVl6MMrPHNMa55s=uO;13(tW0{&>vQ6T8F z+2;nLCD3{1pWRxpQ+i#ldy&JGV3rgvzr1i4cTriuhK4cRmRghIkP=Am` z923;AxQihbR=67^frtum(yc=z znoC7t+2x3!@{*jS2hofx5Zl3I9YA1xo60B*p;v623OOyv>~e+9grxyWiO#eu0B9L= zr|R5(xhNl_LuM3ottH58-`>aj+O(%6EQ0GJrM+KG`N8egv%O|%Co6;qnRQS%Tq^A~ z&J=W-jH%e@#J-kcWPJk%mY%lDO50D{_8b2{Gi{9O(9rUa2UwK==fW>?yp~TG02*A|=bq@0w%f-G16tND`4=|fPfXlj*S?YdWDw^fh z^$liygZSibd9oG?0_b6{D$@t?u6!-!d6ilndZqnu*n_jhpmXQ=rfIgpU=G){BnLIo7W67u?`MlzR%}$H;|Y zDFte3YMs~WIzxjfgtSi+wFnAx)SS;anRSwZIf3rNmjKYP$%ct$WPPso{~rAU!@{0K z3o4U>s!&GXtp3=_r?AwahH|`86JL<1Felllt}qA}i?)IG*hXmu?s-u(eXu47)R_Yo zU1~L{NvxAwkiF`?=WIYUo|g(JYQ>q-IC+R}9XO*wqKv9oIpgse;#>wpZC6;#rneIa zSXKe77 z84~8!@tK@D4lxHp@d<2C5LqN|T{-=qYDSL7d=4f0oj8@DzO^hCqf%kVshr564Nb(>G6fDsjWu zk6C`}HL5h_0sNSyHQS8vIM9thS^_?Tg1f4$2G=~7dR zTVK^tcQ%+nwZ&?5uYsCtNMz{in|yD#46k~kJ+Rw(O8~(!MVEW3c(q<#GN~&x&_(F1 z@${s0YM)`;M~=k}yF}}3Eg|90F~YOZ2t74Z;i;M2nWyF~e}b_Y>g7+8(hYF|8;}en znA+1wsZ|aHEs=kagilOGeCrHKdKq^f^%Xv2W_??Ggfa6u|prKjllcRIJe+yzN-|<_U9dC?a}r6K|=x+1e4xPR3f^KWoE|ap+EKtS+dl# z7|-CGLxA~GPYv#MjQLVoIN(Q!D$+|Mc7&Bgv$n!TEbqm{VT;dtgDKIp-|=D|v5nq< zR{~Q5k{gir!)Av-q!6NIr^3}?^mf)K)~b44UL!+KxH_GlaCJI8AtkXV@daqGmJNym zf1BOl#C8pqB^orsZVc7e*sUivtYdt&6D0n5G)#yS82(&@E25vFqC_)}M-(l5;Mo8y z#MTqoK*%)ros8c;2iM#u*YXp4H@Ia%Picp-To zoQIS;ISJ9C`XgZvgoEQaiY0Y4!?hWL z>U9dLy%1D!2&zx<;4@^t#>~%=`2{k+($N1~kdRx!rwS3zn_giY-3+d)h1R*U0o6oI zM^&MXkzdL7B(Jr&Eg4jBH0OVg%UWHe%a2t`8$Vy7B zL{?I8=`2TftrFQaiYq?S-ygX?6J&_^QIGmxAxYTZSnVd>fKU3a&s(U7dtzQInC7&Q1U$N3XEI zEVW{-W&c(mLua;MmFFZ@Wxl*H!&Nynb`zzlqZ)qiNLIhsW+aawnH_aS_stPJr4qfX1#I-G7AE#oj>2#q7Z|cmjEd#bHb!bgT7; zQHLBi(dT@v%zge25t#D+JJRAc$QHZ}UspZmgR+}(?3WI!atga9K~J`GSg%*r7p*HCAU z;WY+O*>Mx2o(ufdt?p3QC26(Y;&{94AaU?Hjw11 zgGA`J@gi=A^BUkU0^>NtSd(V9ZY(%3%H=igZRGpwt=mSqLhr}>V4E5!Iy~ptST!c- zA70D}lE&f?S60oQ;BYnVUkP@yTit>LrUHjlIkQXWcGCD6D?qtaBc<~Y^>U@sNcpC^ zQcgu+3+6U!wQceCo!h)kFTa@?VdIW4(j zjtp2RsXM`n#3(6eg7|G4Ua3`q10Ck%IOfXnxlg(a>cz2#qjusawW|Q&ID6*0>?be* z1=~BO$h^ zWMCom$hCH>k=oTfvD$=5-_ zAM>^bq8=PcvypDI3hYYoIAGm`C2?2g?1=%mfa@_8{yYR1yo(!EgV!IEOIWGJNF%&X ztTv6DG)%M?SI{&}J4}^fid``x5UclOY^3#p1(S!PEo{X`pn}xZdmhrVdgr5rKCobn zy?RI6tG6h*db`qyqz{ZK#KM&~kg+AwZy=vTiINfxESn==;us5`eU1gLP+})!$6C-) z-?Qv)plCnlq0|_+#k7tF@)|FW4ur!W9`w)EScUakwBNL@MZ=-Y+5MnV*@4VP%_AJ@ zllZ|QPSn5zOMp}k?4@!LRP*0>A`oC$Y>e)9FRM@PZMuOzl_Kkl~ zTp3&zq&So7%Byzlc8IJ!zfU>e3Y)NtqocZ&*3?s=dxOXMqgS|nOS*;Y=j9k{n(k(o&(%Ps_{yw3oY`s(*_*N1?O z-T!4g-B{EdGQbdWDlq>A`_QHMnN^@?#Go`gaXwWaNeIfz5`yBhzSN-5J#+9zQXsFz zj85$$u|C!HT})Ahp_7lnae2Foji*0_cV!49xhqYLg=}-|-)aI9LIZi&(p(e`3~@s> z&8e`&iIYnd=Kn(|GCBhbA6u=Qc)vFC0bxTUra~W=s39v>`EHlWgYMw*l?T&8Bhc=F zf&=jhD3CZ$tYBwF3E#Vp0^bGOgs=G;KFBVyKJs;cJw9NWeoBfg*ycQ_e%TH6@nfoIs|Yxp#q=Ae9|<__*XY!sM4K`ae&r7a4c1Bww9gfl3YBM@(LZytiBs{U$g zF0u1bgTJUXIL41qRZMt)#4b%l3=$YQbQ=shb|sKNh%-_k%Gy9I0z|2sGEQIB&{$Nb0bufNb2bc*RU@k;`=#-y6*!^g~ z0Wnj>)@yV6`il+*OVRc2engeMi~3%PkMkk^EXD|e?#olpF35<|b8_bl;4LeX+RcT_YTahuT-QSqHei z*Rc1Ps^@it+Xp(r?L|An?Ol+=KVusJ{higV`?sq5IiRUX!L&kimF2^Gixk_vwKpyG zP&Xbvr-KzhiJ~avs^R|>?xg)S2(;$|>*eZ0X-4=N+*cpMv3Aen25iT~#f8^K$lGS{ z=NbyC`OKxVjkXz*t2Q9i1aB$I8es_7PZ7|t7Lmv9#A~CP&f93edm-4!CGq0e zv4r?g&W3^}%K?!`$hjN7I-b6Pk~|u2gj6CeUCoy0LvjUARHq={-deV(6Oo8_E7+zk z1PG!e^~n|5wMexvB^?_KkT&wau4OQ~atLhO%-gKJK26Bs9ga=3?+{7OJ9>Sd&^J$c zR^o=QyhM64E{<{$??h$806v2K#~+cG7lyM_94m#mFgT|I5XAyPWBDi>wh=Zp+~Orp z7!Tb<@lq+McASll!97!lOo?s96`qavE@a~{=pw25VOGVwlfz8BX^6D&%O-(($}^pT zu`p%N;*}4gcG*>Fld0b?NRsb^b2eigSKc1IYA&1!sW^;0=F;sY`#L`89k z=+wh@r?R=tMKc3%6=(#-5rl3WKJ(Ri(LSl=i_hCsu!t4FE0jxM^miG+zJPNq3=YcT z8YjtWD)C(XwQU_$?dquHU{I&jfI1(O^eVW(zqQ6|+tzrST_cC1U%hxh2D-|A+hnQT z-=%g$;8MG-rJj}2)>7RTh-8+X76e5vO*f;{zlj`=_~>TAhsF+rg>_R$@Rr98HWj@T z7Ln;K#gRMG`A&rtZvzz=BTz8pPye!Wh_xCb8xYOKk)QVL1nMWoqEoz5&wN0IJApud zh6BQXBeV`3=mjBG8M$)4Sskn7D$qUCGw+ac!9Jl)7`0SBwmPsOX3WQ41lQ4Fx$dW1 z?A%)3(-Ha$-pF4<9^*OOW_#clo4FgN##d=-gvab1ILr3GiU;o?Q+#S~AoE|yd{aaJ zZ$ZNTxMCHTF#-q0Z`NQ|9dRLT)D%z;NFiiG*=ER|*wJv=j6#EHeFD;iX(!#+pmq9kbFpW_4+oh0s^{x^{)Mz0iZEi#8cG>7W+)`)1Wo#3p{h zR=C(s4W5bLfkw^53rHdv+u&a}i}Vr=;y9Fkb;$754Me}=C%1ooO5*vccy30P%F0$} zU>m~#qQgZ9?N~31;CPUU5q(J3*Hbpr$T$LvHQ9l%1>lZLnKYFROBA)gd1@DS32Sqt zZXI2SBZjeYGAd`Hreg5Pc=7ZHqB>WZt2Aq6qu|Zn^H56! zJ5`iGK`vBX6bQ$d7$iW0K%cgyN>B>pBQ)^85}+N*;uW)G@mK%crcob?o7UIGkw>Ho zf7LhW3|?G(M7m*APr>tmbriaGaHzWErPN(>le!dXdrtbr_Obg3e&7?U7t~{}4w65N zM5Q>YX>!}<=b%t4BI~z|j-cnp;(L%@4H27F??u0EOs|4Pw6$q+4c$TDMI8WP|*PNuL$yHo#YVO}Bx@ znx&q<0W-eZKsSU1YYhO}W1!nfe!kqI3Beq>$|f|N+_c}JBV*xysDy-Jo%HcKL@C6G zStD`AkI^rW5QfW-l3ZoJh-C%oE!qH%w+F#dK~Rk^q^yh$NRY%^r1l&WFICFB0_ZLT zxSZkJqE@|wl*80uj9PK7u375^HJQMBLaZm=&iedZU5Es#E{d~0SQ1{9bGlyVGnmQ- zs-k3PcB>278JNnBi7m=W*u=Dk#kY)vKY&2VANZH-5~A;Rs{^O#p*WgAHX8nd$_2X# z-7c)-;xSliRi+N6@`rG_k3bLTGU7+Gzuo4+yMpazZC^`?B^GQ z+oAwNL8QLMfat0m6DLx}w#rjWELwOMKIlOZYP0%6v{9@%JP%+vEDwZl#)W@mNzgs< zhnwwoCT^d@Ep=bqg68CpY?2t{A8zZN>^h{=m>|O!r5D{Hxdx(-NCB?%^>DkAa+&YO zD~PB-NDaj}8d5Qx!_>dKRIe@oYvK3n~vuq{vLRg&j}? z;?In9U$R1aR=dKphWzL0RnoBvYnA6Iq;0;B6q!SKXVA~RoJlmpV_k+pvJ`HwV5B}v zFk<3z41(~uy_8~hDp8)xLPkw*M103i$4}L}*^nSGyJv^;7wnc|EmkO03%SsLmmFf63Nf`+A$^m5ED zfpH5g@F1ro7J5Kkicwu7p~_@Swa^S?or5gfvm$#P{)A)Z`e83XBV{PxmKAxC1Fb17 z4nV8`CiU@ZYUWUuT_st3MU!Qdv@M)&#x#JSo9qoZ*D84pGFVZm`z3+CPwBQhi9*-)@`vT5;1^qy!{!;*9#2qm|!@@)64AzhL=o`jx5&;4DXn5u&}R%E=WwCL&12GpkUl5C>Zye zg6Dgt#{OxCU`RyZ_qu)8gR!MJu>XQ&((U^Pq||Sd5H0Fgd=B&DyaK$frj>e{@GjVa zVS^UPt*EGBe^QSfg632IV3xezNl8Nxv+A%LdAARb_jTggO!poq7rvEGeH&OPUX*zM z(|$M`N9Md~<{g5pQg2*?njjM?+(|7`Gkh7=r#L4YrjnD`@ojM4`Y^L$he>XcdG``JzE zU95jjDzmtCU#^+>)<%?Vjc2JSTi9wge4JBoz>%#aRq|{g7szuMJsdux0Bqx5sdJH# zV-*f_cCk6xII+TSEU2etLNB||=y}cA)uvA>`z#bg1MCpYvK!>Ad7R3HWV%uF1d|}x zCbZ54NsbH?3iq;=%%TRrlrR_)r5IQo!SBPZ5q?k#*^trnl==9dd7nN9?f-l4(}U4& zLR`?guIQ=idOR?JgBi(b_ukzlT1Lyw_;Dn4Xz>a@vth*n8`Zb)D< zePB*Bg|O!~P&BcpHHENe2GS@gnu2BqsS;o}Dhzd#eNFr%ZfhL-6cOvIn~DXm;!vIn zTG0Zl2Cu7N^{ke>ZW`+5BFAmHNDOXaQBO+wKchTVN?!sSwbwxUqaw{XQHkc7s6tpK zs${=J>;Yx6JY!Gb<^Ec&{z}p0e4}(?zRR8YPSlZ9=YUh`8h6?D(6ZSza1vR_cCS^& zNh$a>Y98PV`cURXwgN_kC(3Ztb~W3$L8v|o`dlyHuEF@u4jnI&l~7vOf=UAi78oe- z4Bmw#@sv_aHTN9gPOF668Lm$t-2@mqeaYo)$QE$i46roOG%NNXK>Zf1a7f6y!x|j( zKIm$3KNPVxshiPw>hikNiQq7PX8|z{O|r&2xmdd=(lJ82O0oBaAS_Kjh=PJYl64_-CyDbae_gG~}|q zNfZd>#@Tbxj2;Z2D%xy>4+Bo?JuDdQ_xZcPg8iK%`5Gvj&XbM@)9{u*LJa$;Yt|=_+@dZ=BD@DE zqkm_X5q^P#19z#%)rHe$&US#we+2WVB8NOgh_Br-sqKlA$ELoaCM|<~Mw?B@7u3TP z2@VG{YR-b3a8{TmcNF5{;8IgFAON|zLIb~LLB;L?mvWW~ElR2gMc}u-1Nr7im?F?- zXc!5i5@RyLCqXB$34$3M7(rAo6eCR>k+7{5wyL{ctGbx)Gt?Pajnlj? z&tjC`h{)yLalye5w!sN{HptNkXAeeiYPPi}sbD!`_wdeON9lcTQhN7SbJ4cb%lL70 zHPi2>%8{Lvyi<`y zxm}0x5}Pvhi{t}M3k$fF0ks?9Pe4Vb(7Htkb3p*S98c>`0H*(e*BCH79Q%{ z*chHnw*&FN2jMs2cV@7yoV)_lU$C#D`XHmmw)owOxB7@PK7O$C$9LGJSGIW^aBpGh z;MFYodpt6$H>R^E$n+Wa)}wZu@X#VJ@357cY1F~<5x+JX^oLe=AP5#^i%FA+c_7Ta z8~yI>W~%YW@%jP>f_^HlE`{F5a(O$ZI~s zhaj=bXzR~0qd#^VYfjMTP3<*{o)7JdJqPf_ey4h)oHy0mEaKTwuMy&f*rR~pkyEE8 zbB*vlY^8N0vVujGcex&#Gdy-bCY>Pg%^Jl=OoZ0pD^IxH#dD|nvTf9dbCzC9?w^XD zH)?JMa$;jyonsqG?8vp!=xj_V4Yp&HOQ2lDXK59a!Xa6VDrPEFf{vXkus+MN(M_z+ zE0Op%QVz*+v%)r(FG26Pj4>VRX1k1N)9tvwF2g6P9AzpGr-|-nEPU;F&JN5pFY*Qh z5&f>?s|ZH;egUvwM=%!8MN+MTGVk;`cDr4JgNj7QUIr8g^F1BgY9%{1`v!_}@RD!s z5I=32d^h6Ml#W0<>5F7?W8M=f zPkTR$yO{ri_P(zk{p8e*>YYMt-=o39aLc`sv(t9yuOeV9*p5{SwPN>bdrrw$n?Tm; zMdT16zzZaa_yxSAq{kgdT6+G7gg4w8JdKdKNl2ntg;OE#lIrjZ0e0W7p+>AKERTdL zZbr5CN?A*~hVnET24E(IfT9`0cI%|<4PZy1gagwhTP;v?~XAZ|0(<^YPZrM83evD4VwR&{AgjcN)K!b9)F zQ`;Qz7;Cq-#s>hp4(fu8o0$cB9LMAeQL8VCrk}yBFOKqk>Ml6_ zfDnmA3ElNvoWQO+!}z&=zny#a5Flc@zvoWs04AMIl>4q_l_Hp?rAEc_W5nbnhD=ja4_6| z;T`Fw(#TxpkIZZ?iOg)_E}yX4?M(e$tHq7Kf^;I{P%jX72joMsrT5Uz+vE=D18qTP z1N2)Uf7%vu1|Xdl5hoi{m$rRA3(x)0nN9k%`)?jkE>rat5*)e^;K&)C=|)Xo>O&(d zmf^)gwLKMfn++S_E;e|)uk!AJ(5d~^cW1gnI|YAy5c-(Itp$p>Kl_DK0U0S5rm98` z=U6tL(3eOqsvMTkcB=T2<(>_|S?zM$yR_W%43d1eUA#-nJv`h7DN$EblymM7W?@3b zzAINC9}OGfG4KsSV>iNYARAwkIDo4!*a&r`ADiQe@URNMP4#itC5ow zuzyC%VVUZ_Brxp%d;*+gq8eLtkRPktQ-Nkk@vvtlU?_t|BV09;l$zAXcple4p2tOl z$$6=T&t_o|woGJwQ|O;;?mpDSJx@;uk}r*9AVE1-k2cubT66@{q*mF3<1{TpIrSED zSj%J5>HexQ6{3>eUuQ$Cv>_fzg_uW(`vig?OX+7u&QsM607d0|K6AJlztLrcFA!5! zzPdUEE|!B#p7$HC0)bLeUgRCA(0bVLya4#flCkRa^`+%all zL^`+81How-upnKBJ;b8|$qp%XX|hAB1Sa;|lR9ka^%IJDHpD+i%yXCa!Bxo)nbA>s z9Ga*KKo~ou`64zb=oKfLuZ=B&^*Vvjnmy#OTvO=X<9|w^YqwgBgpIBhx_93I$&?+! zly&X;N$0(LQ;l!ayP?N3@Q=NNtBfmKs^Hn?%O$Q*?X&fLK^J-@D?ddbiR?o2j$ne= z!3G%Ak3 z&e3oo&`B{y8wbQ#J}y#MJvv7p{D~YFC`Gs%y7e431!6fLIwdVO&9%ntAH^c?;gAz$lJngGhr`^j&iFLu}2{g$x;e}062PM(T`C%1I$=E z8d;8BIE_>vNmhzrk$4~vr{(KPCq_r$HMoOn6Sq5n6NOJq9J)isq8m{!&MGN}dLljk zjvihT^>n2pagJ1W7?tU$EmEoON5x79q^ma;>D9?mf*ZWnUzZRnw@awMN87C6w+I6v z$=X0te?T+~8(~uIr6m|bvX82J#OC_cDV{_^oEJ4VwZuppXeD4!^cv5+;BdZ|SiLn<>+5Fn{L=2J z1=FOUsJu=3{8C3#dhg*BS+)m>PBf)w9hiis^f%~SZT$e}CCw%j=?SMb$fKTVP*jTZa?)SXZ9H;_jWnoC%p-Bd zOjtg8gQ{3ftxVx}Ix*8(9W2i%a?zi|{;zM<0CxK#FO`zJ84F`>e+H9@< z%H4@XTnr za!Q62+>12BIT^_K1lCn!3F5cxn1YfNr)EjQsafLY+q5w0Ons3>OWHU!ofCif#Zx)) z2Om787j%`j+Hhlg8CXGQ#|Xe|h-&R)1U>;-+8m!>C43vnq^|uMQtJ66gi@Q3AfHpf zEt$bU_2=Z{7WD)li<@!Vm#wX&6Lj2G1u$)*r-6MsiYeoupvyf5FlxWki8|1_=%XwK z0!bL0#mpfSMeBr|iZ)%%>5A^s{rsM~Go3xtduvw0!j8_!)vA?Td=7y53I7qCS20yS z3Pr11$(G)JD1i>8UPprT%5L*vee&UMwU!UTv&e8UkF=HfmRup;BfJ-Pg?1nLB3Ib{ zBin1r_aMW2NIS`cHZI1YH{;}k#rUwA^TFaf6Ek#CvQ^H|NF2ftb;{;0ihV|MUIy~cMM+xnx9qZj)>>5p1OSLwMcxC)%(RnJ07 zoq`{Slc^JtNHOxqPF83Te5;ZJPI{$gYTjf!vmz;>c48hOFd|yW$KctT75fJ>>{_%z zmn=3}9i3cp9Hic^9SO%|9GFBCOAr`CiHi{dCUL~q=m^?+)Q&-=b|R&5EM@Nli4iud zA|$?bDJT77!I>PmBV=tq*%-U52=exRGDJnh)Myl=iwx4Ubak$w_TQiECRl3|l1ZZ4jTDY?P|v3TsZGoTI&;uPkt(=y zOu4)|`VKhL5gsz5Maq)Y^3_sa>qU?nWTQ(GalSf`J{jk0n>r;tE@|o%K?XSz8kL|t zA+WFxcef+fS=Nd7CCAj12G%td z1tcg2O%%Wu@hmhNyMPCmNZPIQfLA);=%XCwSe zAP{rGt|vNBww$l{3%zkc7~uk4u^`Ju19c2qR3r#kv1gRa1g<3M z5w?pi)xFZ@R`v2Iyq1vYjuH!)K3PFg~SUMCbc)D=(bK> z_w-vQ*FD|V;@QD94%?2d#K?A&^V(ABGnz5DfFw z#)vK9{fWl!md3#p6WBd+h(oazI~%nJ;NMOx57gvRbLVq;a26o?WfnvvMYBG$d&s1I z(8M5=PN;> z;!?!`k_+_^6r**p(!nm%pa%L`02Ug)I$}tIhFw{vondXLM>l}Oj=0Y#6qKg$h4?)c z%a(3vZGG;m|J;8+%3k|lD1XC$q3nOQ&;Peva;|Rz{!8)y7W}^z|3mnHDgNJq|Ci(c zmH2-b{#W7uPw~G7|5J`5Y;*n<4v60^>j@rsTtRN-AZM5AyM()%$#|Iz1Uv@I*t}8B z#z!C;pW}}z-EO(+z)>?w09fA$pr*b92-tVZVW2R=cOfefJ?&y>eLOu3U@HX7_FqL1 zj%7xIu!vM2O$+wLZx>_XA5f<9?zDPi=>sfX^l`A4@pQM*XPt#5T|;}Neu<{Pm9lls zMgnFMnn!p2E7^h2@2G(_p^O>}2<-8Sz`AJ}FXsZAt``{mOMu|hNpJCK*i*?Q7G5o` z{F5Q7x3g)79wciN)|W@r@>+Hw>HhdN(m2XN&T+UCT*+_Uh5xtW5Y$jM{oPNXDkJyY zE%1^Uy_2EyO>6f987|1=mhM&2!`D%u4;HW4B!#np^NbyTfa493wj#df|E+-Z^?E}9|36mvefq* zFjdoO)tZIFdeeeE@!Mtg1C@8B)z7~-Q1oFCXW*B(?1_R-1pCVYSoCgNoR}!3Zbm|% z8>5CfgF$ygk8q-h0Chhg{3L`$rQoH`7ed)vT6y_ZcYJPrJ`Q{o&rI)RC%1lg6IqKqxd*z&$ zG9=iKeD=f3ejKl?MTtSSsGlRjesqi9`aPYHmM#~HB~79w6P2^s=}dT$(D+7kCY!_D zwg%5sC!{tv$Zl=`!+-TSQO*_fL!+a?{PlM2FR_za#)F zmUWH@CF!DkTuA-teyr8?o|S0M73$Ap`ewoDc_X&Ol`!#U7~!e7)dh8dJ5ay@8^veU zM{rWkDt$l`d(L<1QZ^OoUCJ%{)A z;mSXDcqOmMdyE=EHA2syf85oUq^m2awtLIuorVVs=htlawy` z=*>DA7NVc7kRk`w<-CFwK7mSYPGchL*?ep=hG?ey%u{$aWR+(gVm;{3V*vSznq6VWIK5Gj; z4}-IIwUqbaqlsKd+tG>{8J)+&rNN3a&DK|9_sZ_H-=!NlFk8e_7Ls)#+yL_$@nEVvvi)(7~mtA@YhfVPTb@ zVmp@MYShfd?D2Gua(?O!-%f+Pknt=-HSO|OyXE6=XW)sia?B8;<{1pl--AIwp(Fjt}={_(ws$9ToxS_M|1@+(OxHkkvhUOeSo5xr!DWS~sfBdzBVqD)UB zC5=5zqUos*{zM8~A`b&*9-J)gNpT!rUTdNNpGy8;S z*+b8Q6(DiK?5t?nhG(});`q6_?#Ak)-RBNAEhR6!Y?*bO>HcaqzAC>J=m7)jw@sFewJJd(yk~>UmE*#s9Es<*Lo&~Vx6AcSEtiFaE)_+L$l{E}>7Y=o zi*j^6xry&h$;!p29y*l>LVV^53G`?-Te}!PZvc_1XBMzKzB=+KYQY{yQW7>do)KlUzem)OyoQ5W;Lr(+c5tk2_5toIUY&S%1$%dvNt&hG# zq$4WuTZ32!(ELg+#d}EtdS6~qv?&b#$}pSTaUJ)M^zuYamY7uNK6Qi;S~n^FLF^I*adj? z8|n}%qksKFo8)HkilwgrOyQD^H2$G(cwqzv;Zbs3rG1JR`t7VlpB7_otp^#kHk(F6 zH@r^eb|_Q7+{l9J{Yy!(FnA$BxO@r75Qi|c)TYbuoU<%%=rXMNMxYk7hBFtjk`w-Y z@VhdZ+=FVO%muio%?Hns0-?7!kXm*YWYBnc;aTflZ zHvH*A7r!sQphex1EZCxcgap1)G(f(?F5ed?GofBE)&b+0IwO%_%v(L1$H&BQX%o4BV|QeR zM#EK{gYV$vLU1o$Nga_IU27TEJA^!(z+$P9^?}pG3P9ud&@qsG3t;0TQwqo(t9AFGCXh6l{66A$l?22roi4tKH%H(9gJ4$T{FGOAih}YNghuT5N#$Le6F{PTbZXhMfZJ zLA3PSd>Yw<`WVC_jA01o2wcSLDkGir`|2q3cZTp?s($~0jVXtUv%!TFZ-F{TtuGho z?>7qdKn6?(lCAAv>FZe#z~0uK5Ffo%AsVP|nP=3%s)}=tkRscLMuTa<8M#jLjgkQ_ zCXbvq6F7BjSbHg-1mEC*@mYEkhb`kXRD!at>F!%^xaBtegnF4ic0CV+F|p`DDxaw- zC!sxuNlKL_z7o_7u7|Z|eG1S0h|^hl#=axCRJU{qWg>R7dUN@8n?N#N#53}n*X)Xl zbOF`8hhmojEuf;?aajjB1ugY7$PKRQI&4k2Aa&=}m2(iL<>h6o zTyO_0mNvt1ThfHEQ^GE2V7y`uC^mRvhq%G+X0#ONcc9uxMoHVo(;E`KtC=SNo(=$! zO0oz{K6Wjz1G^$wE)CPyMrY6G&B&8p!a$&$$5m5vfz-A8aC=m2mAbTuZ0&0NVh2ja zV8qARN}6y1N=utmJx-~>>0XhH-*l*;S~MFN)7}AaBx6nofMg41q|(b8{~y-g1wN|c zdf?9{SxAs@69kQj5?WMJ6^Wt}9+HJD+?8EK%S#2cD%4sZh`WFfWFc9}&Gxp~inR}0 z)Y{Tke?==#OHDwN02KqY7(gYU)w`~b04;`x-Vj+yCeP=kp|i^# z%<7*W+egKAO=DJ_sTU#N$2#^<5`Q7pP-OIs*r!Cw2v;En`DA$zNocb8Gqr3mUm6ew zd%wI)wu_g^P%0Mu98&<}sD|&mobQI&+P(Z=6yOTBXu&kz^2tlc$E{D{S!ujJ+}om6 zeqV(`*6jN1vBbC4Fw%GmCeY}ZmM&vr;GA}&Kgu9910)}}a?}K{u!f;N$p+R;DIO2= zHfqH@sBH#{tRM>#Jvx~O0v8U(iMhSJ;{B$gy6oXp1NIVs+z+y@I-R$-Ri1F;IMr;1t^r%@}mTVkW$ z1{%<$=t%?Q1U`)@q7gWG&U@k+S%p}(dh|28Lk+Fle+H*MI#imMoKqD4c+>+&qBz+z zg+Iy@es|=|jy{qgngDH8i)Hoq zZ6UNetjmnT)6lbxq0qR6=cHQS5Tg0bg)Q*{tzVdLPIUC;#pwFkaz$Yw7OQ<)N+xtrzrnX@G;PcEH9&WeMhq@u31 z2jnrOofQZ8>PgnzUAfz|wcASfguaDa_~8p%wa5sBILG?^#fSCUPvs#d+&x+6qF~ZP zEwKwpUA&oTxy{&98|$`si##{8_(S#3(l+(jjPb#2W19`?5d9kev#=(&AuRiW7>1zp zKFn=1J<~nv(a9N`1En8}>|w~7Q+Rx;N9C3U=WA<@`HbDp0Y?{B%eE!rMDMy(2l_$*tGc)|a+wm327vSzND((3*T~>JvqScZ3G; z)NJF#Y@ zSt5C%Ao`+cbwxrA&pP=l;>oyA@n;BES6+)j)WRS_obf%-+ZeyvT717Sv&Zq&QEOEY zmO6U9VXLKGlnYIiKjiUd)VX6zL9hS@m$7?UJ8VV&QVSBaG*mX9vBOufrPV7#rBR1g z0j+$i8Y*IC3Y69cv~dkQlZearfU(tQ?3%rdlfeFB?u)f$&65X@&M)D(v-qE?*IliA z1%pK1>gpIfQ(`mIV(TTZS7Nh#6`Qk5Y6#oWTrKqCI!b6 zmnH7E(#hqks8Li|5x=73C??J1`vqd{8tA+qnXJp#*1+?T9|>*heJ-5%-k9@On?NN@ z)dOYX-t0Ga@C>1K?^WsC41X|jhlQ5T#LcKiQch-y6^RL7n(i3N5T8wcL#9}_SmA|R z`F!WcDrE)ZWu@Y7CismiY86p7!<&k?YY5AGq14lRy_7NoE37%+YLAOd3J$;8^MHD` z78NVGd|pjadtj&)aHypUVO8&K1uz%Iq~?X-wJ3*m&reh>Ax6byM&cps&tvRllAcjX zcJwbLIvQw|busgBWf(|mSsW4ps#e8CX)D}^wzOa2-k{2gzH~{om(?`7KgH4t9AwT* zUq)ZpA^n@_09g^w$WRMnUGic@HMO&b>yTsdNjGOS zs{1WQ2%m1eBykM=gC}&Y_+-kpy%YKvWvMB=@PG{C@WSSdU``zsuX4qwRF9t+K9F4# z%PeYO=BojLkqwkFdvE=VE5ln^qie8)LVV0Xy3weu3N){BRc;C4wgdcACWu0sl%$;~ zozs$fN=ko7N@JgaOM92LCSR{Oe%IU~+L}$@Zzx`TAcM7BZLGUz)m=t|=V~ZT9HdhWFGumbn>KTjSlr$8+cRSa&g@@TS34liYja_KISO6Jt)^_apg81!e>tUHcCOTco{4ph8FP)O3MXCiH ze&_IiuPu>jM6}G3)20#uZqr0`%Im%hVUY+|u~J~+IXZraHRkT5^4DtV5+JEYd#cth z#RMcd3*#6k7{k%YEojcI>lMe~y89ctk(gd4iqt@t`xb^p`$Xchu`*uO*f%I=E8>>5 z9z$<(f;?@0oWlILlV^T9&WWlWWIY*ZaH&Xy2pA<@Yh$&M*e9W#@) z8P7>OJ+thGiJm!Hu6oAITTYZ!c)8P0pavLYG8~H{Q*bJ+pArLTvTppd&QpvF?9vQ8 zvPC^b!cTpjEk`kg%6Q_%P+o^Gni2cDrLApQl9?0osiXdm;^s5{rbjOr%VJJBk9r=O zu~r@6bg|F+CB%`=EEHm5Y5SEBORbKy%I}K$TA!6el6qziV{r896EOjG6z`m9LH047 z_oPhp>H~?CV^YZ>>kS2}PA$uakIm64-(YzpeO23x8wq)hH|S!|k!PiCTBh_`xcxT( zv!=2S3ieeBFgJv!!>o1gWdwDQaY?bL`-rNfCd9Tv?@_;O2V#YQs94yH9n(F4T|exYqE*&n2G}U2%)Fx( z+bfN+=hZqb&mv^~f>WLlIk8AVz;Rpv_b%-$=Ec1FVHY9OOM`%y(5h&wqwdo^yp15 zwWCTL5cKE(e%c!0^T^vL#i^)b-K)D0YaO`*>TIq!0d*!wG?uQ3)+8d+$I%qR3F`Ed zTt-QfIyVb-#sex%pA;Qh(dR1Sq0a|m7hCE!KNkA@ioZgiex#+-rx|HQ$8Q4!^vUK2`mjvzDfIb|JQtPc!K8^F6>^}a>(sl@3e&#mxQEhn6?&FaH&1r*y zu_LVwpMI8V!~K6~*M_cA%FHVRB}VPFToVT0D{Xv3_gPi9+kH6@Ru-99O*rwo!aOA{ z&~}ZnMXVW{wfq1}^9lJ{0rT}C>wPI&BOQ75!H=X=Jx7v=_YiLlR&aqRjxvfB&{LB5 ze|*Mf%Ypsq$d{y4qzx=GRqoW*b+#%{Poh@YqFOIlj7!7f2Cz`xgQiMRg~iC}3X9Ex z#WCyk+rZ)v{7KGzFA$LVl*EC8h1bU7;v^Q2soW_n9#;iyEbjTwSnQCZHWs@-RJ-9e z7FQE*4N!0uEn2M$2yi?>uy~$N8Ff>S!Bu}#j5_ieO4~H~Vy57p!ls3A!twrlD)Sj> zGDa%2vC-49`5hpvrxjcWn;#Kyu<0e(z~h4io9m56YgsDaeO>sM&CKk$#hea-21#((H5*&H(O~Ro={XiwrsVT5ICU|x_1-=a+EwWb_J?d^DkQhuO*fmZA z@vMMw1VVqz;%Ne5gOoD!jvSD|$oGi0`|ipa?fY&B;Y8mRsmy2W zyQNap?z3^C&f_+tiAk6^7AeNPTx(VQ0!Jv0gEX~EM8K%QylbvgcDf&T#@07 zSh!MHbWvEmWn(c55Y{9G*WsW50eRpLe%{HaBFYZ#T|lNu(3J(sef(_bLw(jaQL@x- z;8u13;{C~T;3<`*n`9XzSuzq?=7;F50$K92E41}Ux*93=gJpt$!hT@7i-Dud;s7g} zQtwQ@Wo2r*eWkwKNx3VGZD^*df)7{}E>T@LpSHYvebob_h1{v28Rbxj1$M7}~ zZnVR_?~zhwV5QZ+4_Kxw%eID_Tt6@V?3r{lqa`v>=IGIzQ7#z(W?&U}o8_?TR$G3r zV)x)LB4~^dHO^>8m6rB5S;ORs-{|CKJsPrbH>Fj+1tR`vsL4~8eT~?gDM7+vZOKY0 zPAwXt&_x(C;`j2LXZ*XeAiBl+`7Q9zQN=;Bu6~kWe5`GDNvtQ}PG_;l!E_%YV8YG9 zg>{@539{@v zII&)kb52l$HQX4Pm7q!hb{)^Ggh6bNRVzi+D8Ar_0?!`Bd#sCyw|XhKYWy@=c?#$W zDfDAW{A!wADH~K2tF?|1PE;BZlGQ_Ql#DSs8P(pSRo)Lcy#6oD1nC3w83>{Lo=!abyfN(ZEfI9p1T_v4b&Jq$Fmodkxy6o7QZQD zB571n>^)rR{x*BZF2W#Q=f3Sd!8PhG6zZi({oPWR+56$giB9$Lryt7r=#)j?1&|i` zwG@lGj}u6+$=DyY1Mxcn;jqc2&yhRDCT~h9Gw-c^icLNN6tp;(Gd}T0`4|LI;&b`v zEg#Cp9p5LyN3K zT$W5{y(5HewJdZFRoz162uCcYtg&DILWHBuj7m{+m@%oS4Vju?#;aXYLqbt|2B6kb zRg+@I7Avf(QHq+ILLfL%;q6|`&xUIS_1j>-4ic{udAvjV$C1a1lT-4z$HgkC8L^X) z$BzzCQ6i699#fy`@;LH474@Iw@m?t<^7zSKLdt}p!TCMi{^G9PO95f4l+?M{hLKoH z{}hK8(6qC)bR~tA)cLbOaiq>Ck17-r#ivPeM~UbT_oXQj3#Am+1jnkPw>9h0>qKw( zo^^au`))r;I5A@@`d2Z+vkn|#+vAeJodisVh52>JpFhpq?8P=Vr*ywbLQB#QrYzan{)2>S0m zYm>a0b|dIBrIaGMSQt|jpJi2jyFJCL3ELD8PNn;qDAcN>3Hfem1<6&%0V$^BJJhbT zsqG`q8mizq)GkrLzCt&Gyi#-QPsBjKpWmzK#~5I8|NPU_=_mWrL)Cc?KTq9ZqBXWz z@84vjnwS~(6Bbg4Krp_q0NMiKmkOY66e4($J-y!(*0fh*oL1^&e2Be2VNc_J9eEO} zl~x=o^GWsFIfFge6P=dtjo#fW>OS^v$5Sp9>txn&h3<;F-|Lv@kFJ zoXnheC;jxWLJg*__vXM{@+-}we)r=q3f3H*sP{TEOw7t;R)Yc0in5{k&j3chIvH_nR&1B z)7BVuck5UiN2l!OUc~u0GPOHlR<<^ibZN0`-nU|f;1P84>lqW#G!-Vixsa=47_*(5VwFXCr|kj(lsQ>k_q z=)`_PuxxEsEGjvpv@#mVC(NmvY)*~IUJ`0xUKS;NrQ1UTsU5}|8rO;Z5}FYi%=Ik3 zZg%M2dqaJteI%b3FZP758yxH=fC%Tt&r{_%^l{}xVO2;eTIQt$czdHvTO&Smgv{vD zOY>&`gXpS-*TP~ej2S1(jr&@C(M9m=p|*FC02}W5>8d${P zE#wz@A%}gu*YZ0s>fZVH4y+Mg_Z$>JA&K!f>VEa~^c9jWCl8-)pEjaaH-8i+4GdrB zg>vgCZqw?Pt{{e!hu>?TS)J{Z%q%GW&QH{>cg((;xVWM8*b|T9PK;m4tA2E ziVPW{hokPPrzJ+$N$dR4b)tYKj`B%sC6g|qOd<5lK!dgSA8l>&yb=3&fo575ybd96 zNjtE2B;1yUniIDgesX6S?8hFXI|)iQO>1KEUsM{->SqHuLtAnGrT; zF_V~b0`r9$Q0XeYtzXU&^=OW`XUX^FYxy@8X~mN{YZ<6f2OO~)F;-hya;>s{c_m1q zi)RF9h8J3ybEjcOX+an<3yxL3U1eKt0pbhqyN(|{91z~`OA<+ZyWU1HC84GgvizwW zEtZ$SGSs{|vQe6jt(^|(mLH*DQ&pP#m?dHdi6H~Ep1=GMdgp)sd-%3~lt5Za*>O5{JYfKv+ zc6-tK>m#yBL0)?sG}27yuM&P;;`GI*T>L(CUVMD|eva#?lQ4G(1}%RvR{=}`V`a)J z$XG2m|Mcjy5~bdn-S0ELil3d@FEbaeupbHM_Qh2?^ik5&gYFO|1QX_2*?c%peLw0k z>U^a~xk&NlIOnPFM;PBkaU4>sc#!Wv=_zgLATR}ybyB&=;uh;j2ra03wZv3MUgvLB zmw)Smrgl~y{?Z_ zP$eeIsw^dK@$;&^&sRA~-WA|2MHOm#RQ0UP)jh43#wMkbWxYs1rn9^BsA8S6s*-92 zqRNv+1p#A7dS$i2RdhwfyRUEI5FIwJ`PY?59Haj1YM)}YdZLHx} zD_*^dpMdeEz1-8IU2F--S)u66LIh(G)X$d%38#~K3C58^2^nUQ`iTrwKg$Q(p`q&M z*)sVt-Q7m00=kOaIML2B%}zUoEenb?(k)HINcPP?S5eJGS!}ivW!EeuBV#xLT24#Ct5(svWG zW~6eoT2lxxeGIJ5s6;hwo^zk`6xeyi)rT*aoY-@LD}WS@vwAp=6q`rrmineO#8 zS(bh^d_V7eCvGR0LSv+XP?hUvimW1Z>LQof)KN{=&E=BM>VKUyO*L9}VD1pC5KF8J z^Q|4D|5Wv916v*|N&jqOHn1P{*xi z8u&9=9-Ho09}xI@Sw>_y=#9k)GZ>*juU2Uv>h6xZQ!v7$sn2Mqh zOd>t&^FYR`9#497bs@+>7NMjoq%lL;tb8ceCfgUL`zvA14&}Q^`RgeU+1r)x>6G`$ z_T{0Nc>C}hFD`>8fV8#~NM{M8428*yok9v29}A?p3ev`vK(dz7X0`+^7r%7GG-&|H^=#H1+?V1oZlwrx54>c z=lrg8exGrEtDN5j&hOp)GArds?-~&qo%NA>`HSS1_?6NHMr4_a|79wEl^q{e@jr0l zrH#UHoD5~KqyB1+gC(>JX0}ep|wL+Ws!eHtxesB}~w;2ZFq`MyA@nm>+`Y>TL~J3$Xxn#Ssx_D)Vs% zbl2P^@QGj-A87R*m99eeB4p+A#~Fz&u`B348sIlJ<_lnBW1;$4UBnNtW@!~WFOp_P z>||Aac%w)iPK3P*Vf?tzuKgsN`za$Ex$B{utA#WCMytQr61!IIG2Zr!jgU8vYma5y zFFn6PgZ1cg)fD4U03r4MD2cJ9yE8bn%I#zem;ok(L{@P$_-bcaU$Ccr^sK?h zfH=5)w%f_(t9Yx-pFp6#CNV<&28Nm|(C*^pC^CMdB{;%qVG4xce*qwp#qGfz)PRYS zU*8hiMZq|id1_D=_2N2z>CuNCL0!xbA;^Efz z^=lDapos9hTab!AtN1x;3QM&uR}2?5h2gA$BXLY2qpi;UX$`-6)E%t_F9t$z41eJ{ zS#+~J{Ok^*Z87Os@^5KC;fX2&bYjde2TW)N@V+nbQ16EUWUZ_*+X3kbnOnH0w_0NAzR7#yRLtlndv3s%vq2N5k3;Um^>hRYyJorCx-VSAS3X~S&D3b$ELkalR zJYa3)QQMzLI6)F8?D!j^TO!wEITi#nES`7!d-3PvB$2Lf80}-v? zwIdq&zHA)gnT+gKxL18;f7kHN*S*IjccZ)zq-M6*Wgw$E4jY=NH@{{LQy;kQzrolise6=-?^;kkGbtJdCy>k(`RQ=ZcV5G_d<+8Kmwr&nqIgqd*#;iZuI8f1&ki zH6onR>W{uG<7AzH_G{% z;5=O~1ISc4$T&xeneNw)s$vW5&<~-I01;UA5vi4ND%qCX2@0+H$N~~%R02(bF(P5R zL{&1uRO*j$90~}u^*ur)wDl|-YfsLYILxrljpihV~u~AR~ zzt8{8j}ejj37tDzHACv683I|&^sI~Qgq3!}Dt_$t*)29A>+FP@WC>H+hX|t?0xj7L ziFV<&tz8oq5E8~{ct})pHVyBEYHh`DbZ(RKr~_@&d`pC zwx>tu4D_t$7!%;_nLwK0)~G1CKyih}v`1eEjLe}(WU%^NyY<@0C~4iayffQ*T3Bh4mI@azP2j#veN1lQ`NLa*u&-%=YPe#Sva@q4M=cZ=?Ak=-(bA4^~Tz9qa zd0t(zlPq$3^If^;nard~rUNQd_jrEE7BaQF=xL5?tZ3~v_w2=OVVW@a8;GYjTQXQI z1<&QOpxONwx;b5M6s#kDu}-0y$-`elZ@q0>khj*4mDD)?L`x3^1Ag;immoMuTT@WE z^WjSfXG-|O@bTuk=jrCdY}VCgb8DAtA@As=UojQF2MKHeS4S=top z7Vjywl{N*t)klPa^6#knf_q+*8@IWFSEVM3i^HR~7Q$$=pDT5ExozUWgx(Um72Xs! z)$%bGMCJguJ^wN7NF-@xjKwteW`n_w>d4!oQtPo*=XHeCZXm%sg5*GIR&n?EfUG%K#C=nB0rWTPIPsZKtfx{E)zkB6FcnQmAQS`dx=P0 zhMJX}Ci#I2cmBM7uD4Bgv!%VHMJ#_ zKRPFAI#J?PRtYKV2~6@Mnj3q9dX>RQ<^uUxuBIT<-K%p1O!rxKf?7o*8-Z%~UP8pD zp`&DfJ9I?G+tNOTj>KWRe(79D>a4@<1Cz(jqkRe8&{>G1H{VXIR?aOZwUT!>aLb#V zNtqSZ?W%L0y_+8`@-ta3C%`KIjX(AZsV9Osrin`F^LEJ(kxZ@9K8SRSU0gWRdN`m` zF6ndUUl^2TQ4Xn2BX4uq z87e)YNXQY@YbTJQVx#m3wAoAlQALv?XG91lnzJN|<}7o(mcM+R`gyiNeu|YTP%BpU zK~yKDU5g4Ooh`cES2yr0s?s6>9oeZ>E}?pdCN$nVC|$zQq|pNeqiQn`1cm4Bt1GhP1D zIzB%T`Ol%e@q{<)ns<+YTM7jy(g1c^rtu@gz(zT#4JmFn-Pg8BCff>XYo3G#OXC<9 zm#EPwwlG4M-=;!J!?G1UI#Ch^RHy_s%4|i?PH8pp3tAL&7*75PJJ}4`YU80ohUacAKVlK16?mrc*<^p}|Ali3ZXT)?+{lBX zi+qLr@#J6}Z34WivRQqo#wrPJD(i%`VK`CIh@`4Ew^Nj8x?(Z3y-VeTNfJ!tAEG!m zhA1&2DJscU>N(b3mpdbp(IEt|ZYF}w8)xYroD};Pg_8^Ubft_DBea6tm$1>f$`!jG zdRk{wKDC#479lGmQHLxak6lLNlB)&9DEP@c39g2kf=Sb*X|^g3=llUxmyn~7pPa5j zYGm0~TG}KGHe8*`RHkK_pJ7Kk;Y{+`Ob7s&OQ6|vgis5id@k=-dF%r{%!4%2p1%4_ zv8OLAWyC?i_F6*lf$GQtNr*lhmLHBDX_d!VmyY;f7RP%pP0B{uxZXxEwK(2P$Xb`m z(PX_s0M{+~dYf9J`G>!l1KG65->ErPK&4PYctrk%H|~+Iq#fvfH2~6CBYvf_@Y!5t zlLkFQddIq+Js^M7rdCufdK@_Z=yKspJWOzriPKo{!vezjTI3)hEPoZp^R-H4#58Xa zr8c`~WsPXHEnIz%KW5a15A@^B_54t7$)zA$R#N+Mm)z|mTIMG5)g<$EkDsF|m3+;kDn9I5`(aPXU6=cT zIiYb~BXdl}v2JrOG$;IeopPCatk2v6bHW?zl*`m(=V?p(&<yKK$Ge0E;M&uK@QYlg~4PzY)YWo5+i6BDt4 zKvP4(mUryu2AH{Q6(UVf)hesVYfodbDGHLB+*Z0m*VvPxXT8n!X^XSUY2bMpsE`(W z3IHi3+K~!Ab3_kMbr&>ryo_$9(QOz7=r*^urXfDZ*c3k4uVhQ0lshq}f<0K)49Xm% zt?>_Xv7FG>?4-YX>ZPZGz0zFt#PezocFEllze=pO#wncjl;_scVR@2K!<7%66VI(Z z*quCDNlnGUuC)hyk}G#dF6+_92Dwa+T8$JeJ{E?Ab4OW2KT>OvYd}M*d=L`i1a}0B z*kAL>j3GjaMM@vt^^ULYEfysH(oeO>Q0c&Oa;T>Owx+V;#3fK1)1HvhW5Oc_0q7|{ zIsSL4--xqq8tTd0^^D}FiS?NiTt0IKOPz0Kl?u%!!6mu`l4O@F-}rQ7~#_d3Gu$ ziV_$Ej1RQR26lWr%A^|GOte!iwm)TII_k z6wC)*NETZbH^m=SvKZ|?m`_Tkw&rG+a`AN-!w_!WHX(txB943EZ7JBBd9bICY49L! z!gj~c@`%T@YfE;LO?g2>8YlB*O1@^5FNb{HMJ9Xjn8vqqnYOt+cN-qh+WKWwoERWp z%Ubo~1{_LNoEU^)WM3SZQ2v(!kC`$i0hzN zcRo*>`!H*FHn4wy4WjI5ohw-2bDd(uIek5fXu#Sb?TNRg>uunh_#|tr4>^>Nxsn>l zpUE1FcT&umSx{wcCT+Ey%V|%1AJXfkC$!32+|qMbv9v-BiQPzua@j!{|88vwr(9(G zXJbrMD+$wm3910B!E@G1Im0gXi)W!TnRpQ@@*z`vISve#5O*0@#xZ7@a^DdS1FMal1J`;ZoqV6u9gJcEt&B%hJJ}?SQLtz&+F% zb~T(qcF4DyPes1wfYaE=)4^?(SQ=aEfcs8o*gcp6H(S67?S7g7_q%j(iyd&EVqK8N zR(FQoH&WovO<*@60d7G$xS<@hgt^%>Ay*=f<>cRcIB(^)LiQh)L0;JK^~z>!r>7pQSk z)OQ!I=w$cCr__8c(gil~7r(657m3pw=|&$dQgw)h)YYGeDDJRl;@AaZHC)+~Chr$Boz!6PpVwXEEr-R*Fxcj&NfO0qEmUWR7P&`V~VsZ4>1C z90#?Y9sr0An;?;2sVkStS{^(4K5#ob33B22SY6s;{~{fq={NBM9NXmlteBi*e^DiE zD68a~?Gx5pLR%0Oq~jCX$H{pD?0#jh#Y?p{<{~oJ9>~|$E?>x3?pD3_U=hc#A12_- z-5tN7eOWOGP*y;TB?+4#r@7_pzd~}1PV5JA#39F*TNlf0k8RpkRRXk_09RPw!9uFe zX(dd6Q;3LHar5$>D>;BsV4q5{Hw&!3?EP~E$jp1e1Km%tZ>*j5DthTw>&KU=6x$;? zx1V19hlDrW1)L|5+URnI}50vX0MO-Yc8`~di7B7x)x^4ou z@wNmn)XH#(`HqPQjM1xyLfv9+vmrFX~1Z(ekZ$ooflB~r={F0_T;W?2LE+7&hsZ0 zaW4gTyMbnLx$Ebsvc*!&0X0qC6_NWai$7Z>vF1wy!YXw;mkw*>q&gvi)gB%&1Np$C zK=0qeWIA5R@qKM&ex|HG7joyL!4(>$o7}XF>iN2XF95b}3%bPCFer7~ceiB17&@Am z*su9xJ$8;z*0YZB1%9>G^8X!|Vs<-uIngaF%ZLViSVQ2L8wM@qb$ET|syiVeu!j zf^f5SsJD#@TaXHsBZLzd;n`@B+FCel$(t|UW*k9TwH@+rmi(e`$O@q@BfmP(rAqIL zTZ(MlQn*3S&~Zf~x1;l(E)&qY$`bh4&Jy@YvT%i{z6(vy;0AkUb>QQw3A_{;_-M8< zA&+ZT#)MoPW5JeCF?dIKVPodp>6~9~>SUu&E*8y8p783eaJ5g0VZ`o2Fv+>rsa{|M z6zfYq5+_3~i-7n|7ao!OMaZ3{mm`4WshhtJ5&@)o!+QQD5fJJ8Tk$aC=X^a{V*M%& zpffLRJdLU0ihaZY*yiQhqcvxvzDB!C2ae9)@1~go#sDx z-Y7N%5)XB7^R_epxyqx@-zKE!T%MNG8Y!{=na*+_@OBw2w<9cDwfsyi|9a%} zO-S|0%}91`G0J80-uKF_8D8jM(x|a6`8Y$3Rowcv@1JO?wB72OZ1?ySf87QHErNk& zg@-Hlnh$%Oe(e$VN=T=(t@-)MX16%(br(@72{oON9-Uuk1=4a_VSq8!D(BbK+y zI4PWJZQ!B~yHzu4;^%7p%9+m|(o!_A?j@jf25wZa=_E?mcvc1QSPh3jWE5y?ybU98 z@fr(uACaj=lsATGpfOykX^|&MXKVAX$aDn8>9qd|7tfnmSg?)4n*jdk?Bq1@MGpsw zfYBV+to;N7y!6znCIxG^1dY#p#{05vi7sr4jYX|NW^zw|3+J;--_e#l0898>+x6mi zc$J4#Up<4BL0Q0uj84>w>;dxP9#7{by)q?l`C0cV1U&0FX^7mfwH_d>=G1G1)kGbT zl=kX?#8`d*O1iBM^i8M(TE6>0sZ3PmuI@})Thw>-1UxxZz;><#B&bz4PH6$2svB%Iz+>DXdH_GU z^0Z-WEe3Ip*H{bxE|V~g{OF?8%zXD*d`o0M&VnSS>m$FBoVLGp&m{Xrn`UX0qXzno z?S5l3r~J_Uf2|s>d@mHpnX=H8R!et~u5B)gchlEVDuXpuhJ!2!NvAdwV;}gQDKD+m zdj}H%_{oFi$jwcFgftIuc{ig-*3k0TuZ^zro1q4l1Y!nS#?b~*abWcyOVJ8-YwKCz z0909Z7SP-5$4`>4v_)HbtxR%qr-PDH?2>~q{$DQ9QSVUPx0l7II9RNMBincw*`q$n zsaBmgp%TWVfDrHRRu|eRdYmw9bn;*pQHu}Cb^el?l6^6;G60u>HKPfJ9IU(KXDHA6 zlqo!#rKMKh&)eGSy_YG#(OJ!mL~aps6-Y&xfy=0gmpA5@fT<#v+kQ2+6JmObF#tr8VVeDG#RVgEcTq87tRw^Yqc!x@_*-jc~a*7(FmZ8kc zdn;wgl);Q%52Id2Z#Q`OQuVDyZyB&!m~aM*+HMa% zsd1?%UzBJR5`cHHj_0-??I#Hs7P~Hg-=!Z{ah?Z-l^7{JXesyjp63 z)9SrLMFk=1W6o<5RYsJq>{gtBHoOxFPP5Hez5P-M`dS}SLJ5Jb{gn_Y<-21asPK(c=U1$ZUC$3TF+KpR9)R~^vm?@>?4t4MP1U|C9Umyz399a$2 zdo6g*_9q}pFD@yd?-?C8^uAj4WDKoml~wY-Vv<%$)WBsHTqc8$9y+XU2CHk@?bhEM zfAdIhsR~fCt=~f4^ciF&VR%Zp%^F>#=m{Orpx$yzKd7}Ud}Qm$w5<0(t`Z8Vdp$c} z6DJn-`X}LfD5;MPlol5`{<@;)p)LrPKT!Bop4#zW4(EhQq3VhAVt}B%xtJa3$l2tXfpe3O;)`uQBpLZfHmk>qQ}sr z3qVrob^kB)!(%0i8EuvMUwhA>A7XDwy@%1Lo>ZE`q6_O2)Gp?9eZ5v8iW?UtwI$2s zgGVFu(qp`Q_1j3afAFJGWGe8t-?L~fk`(JH5rU@SaYH3Ki+Tn#t4mq|}xJ}E{-%*m2>*7S;+EL~{_aKb3= zJ+zc-YZqo*&(1&ZA!lS-+mKyJqBe>klNs)7*wIt_dH(gL|2h|K8VG5csl-AOwa5r~ zL;j$1msj7{wLCft-ki}C@11(@Xtps#-e5VkCPYRlkX)ZNV9|_&g4mLLV|MRD8}r<*~@IogjK4OP_bP|vt3)e zy@CUgk&797wO=sQ7g}26K3dPIC%w3dDH+1VPA5zQ7~$<ULpUsPMehxhx){P^vVc;cc{X0qfK4~RexQiSc1BCQOdsb)CZb=a>%z2gW zRB#YbAHtllIn%>&0FGA2FG{ABotAd3T+=p=a6>ZnX5FdaSn?&_tRpo(LN>o;7b8m3rA^LZB>`PX> z{{h%%B>(TgYRIirE*6{kP{)KI#!y$h2XB>$@Kz6*;4sg5#3;G4^Ox?H7saQzs=IJf zMhMx$JRM^R1VilRIjKaFH<5I&@I#ICdLb&(pEDhSAQ7K%62fEDc*4z`uHzjwip+x% z>KsNm9KR$j)pJ_vP}RA-$G6kV=}8Xb@O|ac*)Hc)mGk!0Ss$An>b!~_>dILR zR35_G^}qCBi(-ej$o(I{g}Rp1#QysK3gR+yE4SguI^h9Z9&lSR&7n;P`$!c>fD$Xt z0Lr=XE-TJJ^n0v}r{-gsz5FsYr>?U%7Oe^B7)VSe?S<=Hq5SZPOsxVFHe1%QyM=?y zS?k2bIM=Po=r;~nF_eDXd#WqDUgZ{3j$JELgh0|bTrMGQVFru?ZM!Puj^l+u!k6PzCla`+BuW5vM%jBL78v_4kPDxoAKPO}z609lNB4`->4xRMPpgr+7cSgqO@Q7SC;A{E3 z-t3#p3y8UXquDR#=<4*+BU+{Cr`6|I?$fIF@ujWV6}u9FJKpS-&x@W8M6O0jBiQ#9 zwEf2N7Jjsf=PBSfUba-+v&|}p!i9cTWZ>FfG4ub%!76E^G-raD-IcS^}1`7gXKYqK3rsGE%NL zA1jckR$+fRgRn`=ex;3C;heTp>FO#E()|MLy)d{Tm72X3Y31x@kPI}*QtN* z*4V=gtU&48yhudH__Q(a%gFL+<$K5?^kA7%2tqk2gq)ZRJ4WGdb}y9oJfo|nnKa>S zeYm3AL-(Mvx#r?xEF#&jxBEh?h|@8|3DD78ao|2i-RNQ21p)IWSFnbzlFos7+9OW^ z=?kgeZ}z$h56CDuT~KEZ#+2c&Vxts8Gm1HFE^pw;)~M=YP!p2+WWmAN&dz?!xiQHW*NtD7H(w ziLBzc*I`>@4|hSJI`YwH91ixT?j~xSU{1=(gsPKrT$~KzI&}EN{(1d;rH4ruP&`Ej z432SjlS8WqM#WYWl=I zjtG?gU4Do7%+aot{wqW6C(QW=uT=|eT{)LADHFNfud1P`*d%WhQazT4y?I+!njgeL;TX!5A zgv}tS5djA=h@`)aP2zYZy+wHG5o*O}@Q9*af5j(VYd`6!D$PBv;2xw47atFU;zM2S z?X3$J9~VX^$Uw~C81O!Ow@dr!W^~~!ze`Ao852^frj><1&MFI^LQ3w@Dz`FK6JUMi z#{m*x=rO^L;kQElC?U&Sxr&hd!*!v2##|KqABqSqY4AD)Yk3Vn2>>;mnrlB{sl28s z;H}S#Q#qfOoXO)VS$~F~4eHKzh{szfx-SHwBqT<0>@6U>{ z2xAb2Aor!TMr@z*t*P&9S6#WQ2(RDdWp#<>Eypo1M9WGO+Y_%U6XxYB2bDqxl}iN` zW*6CgdJlXlKvXVY;{VLsP=WBw4&i?JVAZNfvL5cFf%`fRa_8lRxPt1v}cXZ%M&j|A%2vHd!xK_QMya3JR;4EE^MjG=2fRQ zJVQ(TUTw(*0wJOmSN)pGPHQQ0WU)`a{; z8AQvlHXfEwp&_7X9K{UK4!QqlV(ozgE%(Y{4*fKA$UISHm zn7|m@-GSYuuB*bazZE1cffD+Z@TBNdvV{_xNt&F*NRQ-lGQ^mdMvT~3hzgkHpzQh- zh~+qtZ_PR+yie>1_Fa;(U$TR!#?+cV92VQLzO(UR?+i5v37oOPqDaf{LaPTyXHGo2 zIooIo78*?z4LQcxiE@}KBxd@L=s2wE6%E-E8!eUfV6m57X2q--{I>AFng7D9lD4X( zM!m;%T(A8yJA6%paYf&))Q4z2cNBG>g6OyAeiHhx(&8u<<<( zZnv$E|tTH(;+EB{_OX8H53wt6HVJqUmFl#~$Fr z3;Qx#Zj*UgiyWcB)OI^cEywE-Hz@K_^+0(poV#y4>s|3Dm1oi2U{fS!DSz}fdgq2R zJvvnvK*r=|y?B=zJH;}D(A2h)k2k;hVXzf5bxuYmZqbX`G6*#I%!>>Dt)cdK*5kXVpN1hGpC~Z3}*1m$p1Dnw{CV061F30;T4KENxBS zb98fVn`c!A2n&4dh0jIeiG%27^P?ZTx7cNLw<59*8Zb zZ)~K%MJsQl?dvGmq7R?EPOErXp}iY>11}h_kbq!yPC8ZxMd3N;# zjNoWrX=8At&n!dp+!-isoOc%OIj2T5{{?M)=Ji}}IqnMH$dbfoYz=+DpV(efN_NKY zs5sb9u2bDY!C0N-R!j)Z6%)!LB2)xHV^!3nOfb9S{jvIJQm9Y>Jt=_p5yZkY3L8<4 zC%4JN$#{~ST5FX$=!kTovsWt>K$QUgng*167KN}7az0rH2au1*R>X- z*wEsiV#cV+vInQ%m|CU;R`_66_&B_ZIp>?0yTZpanP{+J2rbYxa$Lb1;&+G)(jp76 ze#M^PkEGKtsFsuS{YHR;tCH`M_^|epMzV;2;UJ0Sz0d&{LYJS6++v=P!(TG0r{=_a z)Qx+Xg=1GI&gZHN7$LeE>6Ds&k~TebuD6(FWy}2i-^(0A#NUXB_fk|nWUWOHRQBlzj)&JrqL>pXded%k8r1 zE{->iRN(%Ti{o>pl!=Su?SBwN(^bTF^FiWW93LJCAG*$Ge2&6$if#JrsP{NBS?*82 zD|>LVH}MTtan=2}TN6HUg;seEq40_6yynbDtP6iY_R79g%k{tE+S<&K)2GAiCE-V0 z!$QBU?o+vM!3uUk_QP`(HE&e3o;UwVZ4Gn%1-}{Q!i>?k(DRx*CQcOm=G@GHIqj5~ zkLF)}gW3H;apHNzU%E}JycHRyt;rfe{rl+r5mdj6Ur~^tYC{$|6`ze_rV4!rrwzNAItAKffD`4ym6mQj*0 zM3-fk3Wsz9j5H}zz?nlz<>F`sM{m~5bSbibo*?^2LUxr%ht0G*tTB;TpG>5~X4)P0 z=S1QkB(b8N4y&WX)NUJe)s`+$9hP0{FKxod8;y?rfSidAgTWHL@zv~Qsz1Pt{+NYZ zfs%dTynM6n`ap0O)U-9@T3AL2Nzt=@-=pXS(eglTFH zgSb`368;IG4R=IE9VyRKLNO~uk;CD*& zvJ+@@T{cxw%mX_Ek5P1zP^d~dl%a#g$hTA!7-jS8AnH7?WtY_8TOimA`q)yB_pX5* zYr{Yg`5R&Q=PV^Un*aVl+F`&YWrL0ppeJzIjkhobDq~+@hnBA4n0_Id>H<)>#1$w6WQ~x zT*piTzZ}ucks@C8JBO@A!nJ)Ywlz8!8|@~aS>83rUs^M7h%$lt%yHQgx>!By)gfP| zq;!u5mBz#O{ChaWkSQ}`ah<*5WrmD=(291&UTlG%H<)D)vApU#h2>Q@pT`X2<*tP1 zLJt-S!_&uaJV-x2JkW3W2K$X0hn7`6SXSrpWYl>^vmO2B(DoY+25w$9SwRMz9Bkrv z-CnDHg&n{%%wFpjmBMKcG2y(dQdm=BO0-tk0WPo2~t;nhGAuaOtMk^$Iql>TM~zp! zkwG0icT)Z##~{=xnMS_UQzz$1RBcI3WJ~CK&QmAdtZX7HS|15kB%V2WgvR@144Q%w zzP_^fVah?V?9QocNd&|aBIIt@`|9NAaclRlq|Z9*AK`ANe>mQX*YD!|F4%GCn$8p3|Bd5?IBS;dD~+;Ocmp`dPk0{XDx$ehO~G(lE8i zdA3o@?}|Yn-^ChNOaT~+IRC8ie=4TC$yh@IE%su8NlIILLz%W_>S&x)(CKvH1An-5>%5hnFj#i5-6o_)L>Eg* zt-@-xLYeyF$n)wO%gU?2*x^0CvkRk;E5cTF)O&(V@ zd1!gntkNp)Fi-eoW@v^?r;~NwP~u8JYjV7coch$7l?h%I3sS(X*)r=93bNn-SfV2cz5XG=$GJztat|uT>5!6 zqVcIut(@tRV5@8X1@kWrFC52WAOdY*&own{dwDBA3SJnWJ9}BlmbjKaK1k!RM&(DL zDOl?`&C5`zTq%FoOwK>GHGDjKK5E-e7BUZk3}1(=+O3iJ`QP1{#}XrAvS*5Yqi&))sV9E4qB*|kMD){Moc)&-ga(y;T!?jn zr6;_gFsrQaXjxHRz`QxTqzyIZ)|287X?99Q5p_tblog+(e%(P)(kicVQcj(ga-Nfd z(+FVL3olWZP;x@|#!k=?O1Wk>u$AkN`OtakUDczK-c2NJx05m@>6A*skYJbkvz;XV zW^Sb(2>x6mN$~*d0}crKi<^9`*<|UVE$yv%Zu7}N>8?=s_;*ThvbBxx;M8`6@fIHS z6xE52lN`^~alRMwoSW7np-<@S_!U~eGHFlnV!|AaYpe0KZ4R($u`PZPBep+Kc#m{m zK}-<01KLuVY}EFi>}tAp%gzSRN*~V*Udvxx9JA29goU!Nv^Lnq6D}3MNaSzMvQ@WX}fkfld&L=qo&&AZd~&=AKqbz_eBFOIq{*H$HKws290JY^tajJ^k*?|jiq~>5iUFM5UXKMuBjrTue*37tDF~Rj__iJ_6 z9$$3QfE=H(%U8TjTXLCH#(Wltb{~Kk$i;yc&SFkrMV;KLEs?3#Mpns##|9K;c?&=H z6usp)K45+~$F?!&jBE2&ejdEi6K+Kk=-^YsN^taeU@7oD6Qx+nG_ zL3JOF5{yR~#dg`ZC7(H#!<$o2(Q8_z8pU*Er9Fzreoe~PD&={yN*7m(eOX|G#z`@C zJV98@1&^!vdX`i?QqyP;Z1I;a?y>OJay)or=k0PG(X-fGEIz>=yWaMW-o|lUCixZ_ zzuKB}X=%tU95p~Vs-McyfbAyqq4Kd99|SI^;$j8)S}ygQeooG^k(r?OU%|@f+Tu~8 z4C`>f)f8a+ayS&v43>(WkH^Tl4Z_~3591-Ddf1@Q&<>auZYiqO*3{ZkKQXR<(A6#? z<2o~aT(4(x%W%fEyhh0OIS0DN!2mbp2aKtO0pm6q*W5oGC*!)2aovojc*9WsjFXWq z7kN`tkoDEGGIEyP%^A_x_+Q0;xo-I<{r~;das9 zBu_i|)i;a|AIl29KXmK-J+|IHYZceV(3>t_2=?vfbK9OC)G?$peF6#KTIB^~Qwq5j zk;@DSW%P5@*QM9Pn!Y$F%8p%3w5@;Cjq_xN7tzlVgE$8Us|^KX zgj-uG(dbC)FCk8QfiwJ1@(~klNAWWya!`qEqY-Pb<5o4xV6?;)5O}3?D>7ec_W6?r z6cI6fz+kO-MiJ`-)(QdE3K+VhQ<1s5bQzrUWN^Az_RH-r-r2Dhs#ily zkXd?=5MufYWlW)QqP(Ov4zyL{I5DC0+Ra#0TsiS#*`XB^cDx@Oc%9`zG~4%|q{laF zOBPd)--W>8_QGvBN=ziqlc0QZ#l@IVXfHFzmcc(=`OjjhU;>Mzu|=^z%008FqE@iH z9i==_O5Ai9lEciq@@wfL_pMxQ*f5YPtpkggsLrw;LGxxahdDf-Wgwa*&17Y6WvbBI z6y6Tu^57o%w)nn;Z#lmd;Q#CVf1m#?{J#oW+l~Ld_&jvk!?ZYB1n7}0}Kq+Mqy$SgdvOk!^akdz6*!2nKh-5P6_5(pMRiC8#j_ZnDoJ+ zaf*YYQwFj(BUwtVt#+0{@j=z&3Wkj>kRx&NZzKVplMb-A^-GmI*uCT^!W&&hTo?c< zerVZ3+OUcL+xegPZYS?5K0_bFPX(4RpYeB=Ud?P31qWG+b~v^Txtdi;Kvun6UzW?F z-d3S6>k!K-`<~ESbvTPXE0InzCd&~)6b|MVGRj8WQ`@AQ@nWIwJ)x3i5${}l+Wx*Q z36J79xZG&DNcJM-#sELOP*VHPd{6FMt1azY zD{pIGqg6l@nE_0KcnR2IkXnOUJ>z%@Y9T=&-*4@6<`%GT`~AP?56|SBeL4HO_S$Q& zy)NbOB=gWg32(LQ?aaTv35D1mF~uNi2f<6VwOCvbor8t9SejsFHTui(k4A)E?h*qI zEup6;AzkfIoLxmk#uLYtZLckgH)+b|LhaasgfK5nd~1~>SEkEs+Hy7)8x|U#moZ5Q zFHW_O8f|WE?K!G_wR*DtJ=;NAtmxwUucSd*W1Kwn;njH8KrTgA*-p0|?8~o@+sa8? zyQ_F}{bn&5WmTuYrSH-^^5fr;dw9r-%JI^t<~` zUt}RX+iqWF%!hrku!u+?1sGz$e39$fL4H)^iChi3nhXD8vu8gS?R}p82>8?i!APFK zUDxMw%-xM?G~QsoN|>9@kE3#zg3|hPPY=? zO*h`oF0fCJUULD$C&=K|*|dA)beRkwbzIZH4)on|$Y2;0LnjdTHSifIGEi~*!aGzu ze$Rj+heJEIE~*0p7GQkc?z*@n&x6oJ;5Fab!XH{_R*Pb2kWrT-+T?NRxiM>fMCQJ2 zvfz4nXY_|hf6~rFK{U!7V^14<1DW(is3|XUCbz?P^w^QV9khnM1F1{nHC-?k5gUl$ z$c9`1FC0~kXH?1&=c!u+Kh6`uap1=jSy;v++U5-*YD8vdftN7`zyPBY z86u9K%;IfbzRJ%8elTxsLJ|T>cR~ErbzrIdM0x2%M6jLPh>I-#7w}V%2mG`yJl%(I zzrL`{P4|izhbk%p?-pF(vt#79ec@jdm&hCglEO117lO0`MboQ%)w{gf5^c;MIaC_5+So(R$E2+EJ zg=JP(b6VrWd1ka$lks4l)i3~a-M+|N&S9qkG>Ko*<{bJrQ;f7!NNEuu=W`heY=SY8 zIr#xycEw$bOwf9i>^^u}egW~{5Rm0oz#_SL#6_Oyvc5ovCqD?HP@^VIN;O9T8OwMw zj66n=`MW0!c=`0{NVCv&S`prIOAbo$+F9$9`wc%p4;xoL@aPMGvKnWy6)3tK)(=pW zJc;+0LG=Pr*DtqVt(rtGdIbJvs<VJOEwa?672HR|t%bR|=4x;zUJ~A$goiMw6+Fc(M*r)h zz))??#p3YbLrE~bfQ^wG-;c!5^H<^pCY5cV=|4fT4TyJ@BnOS&YB>QT=eFX_+S<+4 zoiKja1s$^UpC%g^T9KV`W8fg!jxhqw-c-L$=>WPoi-VMP1Ech56<6^TD_XAfME=!? zma8vXexdOhY;fzYrOWt|fFiATQrvtUCJ%F>%Mx)rWn$D`?A1T^yr@c{9HnF0m`7rw zhCey*<&7@0yPA(j8MFjc`8n6F>N?R<*!(PTEbNgbSOsoVI(%qpCX4AE*?jt^mck|j z^r?*FCEVevaoo>1Vlewm8OKjey*PLSY9v1ml%-WFxe`kbL*EV!%1m36C)3e%%dw-4 z8SRXw<@^O>`SikkCdY1c9tb5Hfer{24Y;v$kzhLcF!l$Q(el|+9 zALTK%YUs7eJB0;w#XPm^imK+AD^1yDCq*5WfedaCHwukg%#E4dQklGvV_+AHxC=bV z`VX!Hu}mAiRroC<UJGr(Ey**`U{ceS*&{`unPQnnJwZQHIpnA;Rf=5X1PGF}PoTXQ~6j%xbkAb98GxR~CV8 z-s8}hw${^%tq*Rh{aZkNNWpqdfp(W^v#-@<8tvP{REpKU7!R!Va6l6)EnI0fy_6>V zdyIMla;5n&Ey%%(+@u+Siuse<3Z}8o_r-ib02PrWYAN07f)z8hD7gV@u*;Zs zxxkYN9Lbj$$enM?0K1Elrx3XH9Wx--J^UaIxhc3wEoZ@L68q*g606Xr*qh)I04}TI z!0Pe2iT|eVG)h(WB69I4$F^oW>iZ|5zJDdxX3!fRel@`E=Y6xQ66aA(^Ea`dHcBpp zhG@&*kZPlj-}G{t9>TkML3BRZJU0ycDOV?OVvv^WA^BL66A_>+^34K$&M>I!c42K? zE_wC3)nrRT-2;Lx$Oj?->|RIIm8@@GV^~=z<6u}p)LiN!stY70j?JX{U!X`zlsa;$ zfrz?S6K4N$7fIL`#Wv(fc!|K3X^H4&f%ZtS-Pp(|DtsyDw{-*(K1qr?ez4ypdmPJo zdCn<~H}4@F4@!jU%?7jjl@c*Xay(&1JRlK+B|LO+rG zlAQbjq2HxL^9Zex(D9P?EkYY5R3f2TLK`GhETIbtt&NKh=mS>g5xB5 z9MOj$0&YMs{9P-dQWDN0@-AebHXn4M2ecY)+2dVxNB7MbP)p^<_f>BB?m)e zyF%}aZ1(%^;)AaE`-SC`Ah&9ZyF^S|M6qgl^wl|D$mp#b| zpjH`{Tzq!@fk{FF6hwJ2x^%ja@Rr(MACb2HKnr$Lr6jP`gTtsw_yKNC@?473mlTKogQl zKuD;EhlQTLg4nXAjD#Y}HIbBAHd?<@6CT&BgyhYc+Re5&zG>EOm=)z8QIT1m4&SDu_YFX2tD5?*s%*s}C$3B^TN#OCAUmFP@ISkMSFqP9d}yp5+Ds zEr3uSvK=BvBPFnb6|%@>m6`jWG8?yEKpy*4M*dv@m>eZfMW4zzy-`9!&ztsQ<+{+L za{%2mH_=O4YWeo(PuyaiFR{ePmECJz)L>whg6f9f1ozXtbHR?Q>skD`XpLLxpf5VN z5Eanqu)*jMdMmf0S?I0AT6&l?ubmoShZCPgFDYXu`PjA?t{8PnJT6b=)^m!}NU?|3R!Bz~sX%d5M`99FpbQOASmtPvGs z7j3ryOc$|t9fGXQ-DyG)o36DNOBl2tGoE=sO6R64+?G$ZO8xE&?cM;(jCb^gjGu%B z1eAtBuQ5k8CU{cTgg%(eo=_d0rg6lXQ|@1;t8%PDAHCCps}unB{dAtM7{K$qVfnQLBIS#z&@7TnkEZEmzn&a2ARWpWQeE z3FOx{;e*t@OIy>CNYIq^tQDNAP8IqZsw+1KTI;RhrolpVtIWa_N6{>3>l<5|1;>S@3S@ zBrgZsut{xq#S=y1b9n51$eD_4IqZRBYA;4TdvP9q$%RjFInDG;#FiGD*=6z`o%j@) zJ@#I;zi<^g5-)4xhHw_K2fGvY-1_h{7#&nE`31`3!Cz`?U&hvObmF7PbPE3r>3HO4 z5kMZUt>Li#dgZPkUYHn9ZRUB6racEC=oRxiA5(Pby=BP7VzD1dagOwDdBtB;?$VkD z^1&F29eJNnjn(f2A5X|W?s{RN>gQk0vG^sqWB{mnIkyHm18^xH_8)-BY5?X?t!E-+ z4+C(uyhkVABDKf>aLnr8e`$JVeXIUgt468*Ka%SI^HSf@`+t7&5;`wXDV2@wJucOM ziau)p?;Nc9-a$s``ycUdxG;HF^q$JA4)uk5D$s0-HgEIr-aF8xJrvishxZ(=?>!l0 zr>%@fe{t<-lD@*30K zo^9cYwPo$KW!uZ*wPm}@wgrx{+}SOI+S5BkRbfMC_=^$oWM24-0j7s};8INl2DZ}u zLpu}V3D#%uidwx#?2MUuY_gyKngN)Q9~@t+$E=&Cs`VU&SV{$;M*!<18KKmT)A~9> zqx+IS0>!5J!xYSsfMMwl)cK+d?9D~uHdrX672Jl+U!iXcy@!Pa6og(uyj@!}8=#8i zh2Le(wrBxQ_D#7LL>id+GJC1^D^r5c+^eL~ zqK~$Z8TUoUK2vQM4o@0hJ&{X15lq3y-zyN*G8CIgeFzue76tmdo2cL?p2)}_@D=Y` zUCN(j{FLFywlc6% zh6k!!l$L)gx%3k}ZAgFfPyeCQ6LB<4i!<#-+^pN9JvC?%(ZRiVuj6RA-0|u-jwrO@ z5bYBC2H&70wO-KK|Fx=tv7GogxkPxh@%L6YuDw>eG4Xyg-t5MDicsR5BrGMt_#WT8 z^fs63$P0Wa05H{f8G)nhj=VxR)sdGamtLLfNUhnEm{gN$|DOp2_ttXdg;?KmM+rgc zzyxY{Q~n*+{lw@U93`MNp3_8a5UC9Qs^ZM z4=8$?Rg!=(0_uM#|YT5K%?nL9jS}e2+@%R9D|o$1*icN{2IGezXdrFm3-;ELjRC=U>== zm9Lcj*E_5x<=}(+&Hl7Ve@>D9oUHnjix2QO!MQIwXp`hRfm|r4&P@c^3y#1Slg#qU zI4YRsy$1ZGR-U!~jvS9$A*R1(JeJnyjK{3Zc-&}ylsz83aMb^o@t7%J$#^&xq{kzR zf5#CC{$1kp40{9rG-8jmM(|sWDQ{u&Y^KVYXcql7z3Ol#5*okB-P7vsr4?M!;&GE? zWp<3LmGTa7U_KiMJ%Y-N(eSN8b-h@c?g@-*u{UfD;)=xG9r4VDqA3)Y5Ygdw9{UjH z(cGAtHTN;og##5*nI6~M0pFU8n}}Th1Ij=x?mq2-X(Hf`Mkni}6=oyW4a{TR7}W*g zmZ~n)_4fe6LO8){I_$}0P4iTHlW^A41#DBt2q{o^W#XE-4>Q0c&aX(3^ ziLH}&n9Am)iVc*jelDqPk;`#>frJW3>&=hBGVt@VKKs5ZeSlOemQ(f`r>U=6LraTo z&DRzME&?ES2hXAH3LZx8gSkZK_VO^{0V@do;bCf#V!ifYtPL8g+wlC4U|@v@yA2ls zwu1gnMNIfE6ZkGu`#0O&EC3|e$q9g&a&n>>d)efyV#Orhy9AL1vV0%BMaUDm(0C=2 zDJ2Dzc#*jN>j@7TiU+FJ)8h#l@-#5^TTiqg>Uf4Msi9vb?+kHfP03)t#mT^0fKO&+ z{w42bMi}5^*lRpR#;;v}_X2c`cRi2)Z2hJD^9V;@e&gnP`nob0)#S(EUxSmIAkYjx zZnh)4HDt-TCM9jiQcY3PS&N~7Zn)OL1F|JV11~G(T|-iYFi40bNmh!XQsm|!ac^$Q zJESDOpBwj9Cij)ODce;_iIj6mZpvnrQYkJ|v+@($OcVXt;TTEmyD_5#?WT3Pi{pez)@zDS$`$ ziNlKR{F3~HA3us;HNQ*wiP7N(e!t}RG(QBJ`nvd;`?3CyY?nbf_fzKn_8f4Z21ska zNYTREJdr%l_{l|Dw3jw=4GH%aH%ikXG>65#Z82(5C$KcVIe&8a?#X$`*VWjAsK-M6 zoYg5}p>r^5g=Rja7RUjMhg<+EyE`k1z#hs#pT1qupPr}-e!~#%yN83QJk@)HpCkj> z^mjO(7dik#kP{=EJiI#WMc?CFp#z^F0-cTuxWu)8(YShuT&>Qv@I;--B=G{ zQ0U}X+)SGMRYe~x5d68oq7RhF;w!7uNV415PJqqsC?bo|8lgkhu`)nOazHXg8dc>` z3T(N?cdsPPlvEr|9#c}$v{HY%oQ;&QlDmszkfDx2rZKRX9EAwC7?V{?GG1D0&Bg?d zr_h+()RGgmJL&8Kp(D5fZHv*W)ahwDhk*oq?v! z!94BWJ;Y=A8w2dfU*!9vf$~G>-j%aNb{Vz5>FbMsFi?z5ud>yDpoAyeOySAC;{Ekp zaM^A8;rOS=hCdjv-aN_fGBiH&M5Z`dXDExzaQY(o`T=+y@pnu0-uRw@_0%}YR=gd9 zU-cjIW-ER@dfz48p-{-BmjE!ke89)ctE7+_m{hMl1%(Mp%jd!xzmVO@GL_|Bku2v$CzE9Xwym`L1v>RcVeOn5&hW}Dk`D29T5GpM+ z4stGQQzer~L)5Df6IC3+VkT9=UE(-NRdx8%#zsTTo9Oq~V0n`fL-+kEM%;k!y~0Y~IgXR<7GSD7tvL&H{CSut6s zA@Yv8@r`g1Oi!y9V zOkkeGXbtpS{E>(5IvvSl2wz+BDpIku5o_4$!e+9&DE=Z9*re7P&E2mScU3&F(&~+J z)#EN~n#TCL3zB1{{9sAwu5w$jP>K&sP%RXPLDE8IW`JN2^RqnjGuwQ9T5;R7k}F8S z6r&~8@;SJFF9CPitML0m%=K|{&!7x zLe%BAMP>|(EE;jljH2SbSPT$z(DANe=un;>e{Z-*sMYFdu;`+Dmb%H7Tk*RAT_r_Z zBpMpZAmV>P(nyPOL1b+e1Eie|<7lFBJ&B4O#~4HQq$XyOP@}DBrKPs~#CJ>rAq_9} zA2)IONZr%dYLy$h3q}38^4Tu)`KJ!^`B=<+KH6eFSFSRj(G}*iF~pP47pTXx^LTi| z?{|R@xIk=cC=Us?;~#E;uR|yOW;FE8NUh?&@)abaC{>1@l#>k8s`!I?zw?UOTIGY~ z&OB0`SIpC#PlhN#t9+uoo7vNxw=a-)n@WCim6a4zNo!ZA;4|f2D!4HwLFd{Q9<`eD z{w@`KvO^ve(LpAne+_n)=R-n?@_e$)3_s4LvV=F*n9sGs7bbb0JotQ-8NGbA86C2s zSD3*CR;<#Il$G7+3-rpBz)*=A6EL+S!E*kz^Gn86M!SY`&*K#`J~uIl*}>^TIKJ`B@Bvxq+CjgET6|;5N3_$e0;Xb zd1HAMXt$HoxYj5t3!3wh@^bYwztx8PuKW>I)+6OBcv6-*>%5p?fO2LHMa~1;qsm3} zHRZGCYcItysc~MgT;A;SWtWhuwY4#cMRxF|c%F)#POPZwbbI=2+K-j>3|gRS9P&uH z91^q&vu3UGk@6P8=z-4D?<)`Ss;Z!ZGB>?_2pgtb9Vb5dSN9f%5#>muC2XMc zsb>{WY1EBo{Joc1@hhzO1}ofRg#%VNWQBich38q}2d(gIE4;-DFR;STSm7Eg{Js?~ zv%-I|!cHq(AhS(7tE}*sR=Uj!D;SgUqQBCQVOD+tV+q$=tT2?vkddTE%z*DoWF&Vgy_# zsoLK%ZdJ7hA2l8Xyu&@m28%5*$K-UQo+vux*Z?ghT`EMz62H{?)M3eU)s|rjn@WmT zUkv@)TIz{Ds2ZQDbFZYVsIaz+3TykQ zP}_+o`dAUWjLV@qmBCpvo!B-nE& zGC~_g)-Y?lF@NrfCz0a)%Cp_HHdUvs9pM?hZ+`eIMS37MFRxm|kNH|#dyNP2v3_ZR z#S)K=vs65q4)PlKw!{}wwS*Y;kl5W3TT9uh>-qQ9n%(*swny9nm6sKqx zz^xrRx!IS;9WBIH?>7QNG3*f-46(?iEt;}Jq32^*(@?{;>`#ioY z7M6-R$~U6S8(2n-)@8UWtIm%Zf^?ln`L+lfxv}8nR|t}w#5ggiu^W_Od>Alwd85$OCb)?9l_mM znJ!FcD)of#!W(_VVS$CVDNY`?#9j$xiY!Xib1U=VQ|DJ;_wzqW?Y*|3*Q{kSvvGK< zs539N%%>sOGTe}&ZYXK?*thrXp?}PUTnuEHWvF$c(LBYuBJoy!Q~3!mubQ98&*GG@ZyCQw z_}$OX;C~)+UDC`uFxTj3h1&7qf1I%8_y(l^lr>eOrAizm=tx5 zdr4b<3opvC-PLkg^+!P>P!_bo>IYP zV(e3n{fj9|PI@B1nmCh4vElSdaEAK+IcY#}v9@Ng3GUVC;Q;B1GDRi*hG7vz^p!@x zSWFNV_A6kwq5y?uNUioQ3|yR0lsp4NQK(dsitfHBy&pASo;h~OVx?6t{7av`3zJM< z>d^K?`+1@;zm;yVhv4riO%3WxCKQDdJmftPdOFR}q*PB%a zk5L2?!-w@y#@d?H#*2XS#FKm^GkLarriS!PhE%wF17!V|aeFLuVHQ5LRQfNWy!_-M z^>@6wk&Mh@Y;v(dvh-_Gz`rs1G;lsz{AntuUFe^|>|H35M5dNOzjInC)SNTLc-zb> zNhnJWNNI>1#e9YwCn%W>qAySE9 z7`DRP`px^kd5Sgleo-F6x*gI#2` z@D1sJ(q_;aCdd~$E;0vwoBYP%i>26!qeM_pEVMO1@>4-y@IoVEl~QVzB1ROsRTlA_ z200{w%%uWhxQdf_ed1fvHtuabh7U%dbiSm)VOvePqH<%rF}8 z3H|CDzMofGXr4o4F&l&W^+A)`>G(2_UggmpFz0N^Gbv=H6tbE^jNJ@kYU?omLZEN> zDzjL!EtPEbGKO4^RESj{OK3syQ~_scc)~|p$ox&oe3@j94w^`#*OyC+PWyox-U`Rn zj8Xb>;!ja?(3KF5b2W^__j5O>G`}lln_RAiA{TM#>@>#EN)hfdMrGRGX_QKA^xCoz z!cFiHmtJfOT#Il~g-zT#on(B$IU-F6e@sBkz?)upxS)!o$;BQNJlWWm$tS%XmpTI_ z){!9FX*vdV9Y`P!d?ES?q0?;(+z%O-w@`;AYx72CVqb2mv(G7iJ>ffxY=LwA5s`tr z)9GBI8P}&Og8#>L{G|l_>Ff9zgp8U@j&7rxfa;FccrN3en8=^3Aob>(%YS8U~aL zx+6s{3=#{+)RZU2{EgS%x&EKD0`V>pglG;i^I~0-U8b%lc&%u77l!K&76#U0gFsv3 z?ybd~Jl{cmh4OE4*O_8my#*;zR%1t**y!+Yc9A9pgRcE?c>QwKho2Bolyl5w+#W02H zMxh~n|F|h)qQev-+d>#4oLKWr`6P13tP^Jo+vWh{fqozSRDH08^RU*iiT=1+3(5yK zyZ5=8YRkpf(wFFu)|TUKsau}dcE6i}k2y@=lKP+l;;o?xhxD3O~gKPCWxWWDo)4kkP9U)8#$S{iOr(dc>e%6`^@Gv zSoRoY1dQRCm~J{(p$-6rM5daNy`LK&>gLqi3Z%xkkszGf=@4AwQg2I>`)?6z?!EOp8MYb$Pu>@lJtw`9 zl@aGguBKvsTg^DTt+nQ2P{sRAo=F&f(LLi^YibBXIZZQZA`jlDp`2W;H6G#*H+g0e ze-Mhw&&|E=;Ye%>EK}_te#0&Wf5kDH`jxQqovI<|c1Ha%WB0u|v!3Rcm?Q}b8H!~h zRGnPdic~4#VXZMwZp7NA7J=I#J}!5LmR7+Eky}qSR&N?&lAWQgV7m(w+w88Q z`w&DgH%}kX$*DC93kI8={{X zS}KjXg-L357dE-6N=(DMw}8Px!mU%QX!?O^(Sk8vjAO9FB4N^{HMCL)0bgZ&os@?Y zsdm5qPjBU`+C2`6!iZ%Fx~*s7Z($6EVa9L|n$i!!SIKD0806w_h3>wfZ=lw&pCWiY z%&S~K@Op^X&w2HRUqVl{?I2MH2cpAtFu2iQ-HIPtuQsjKTNy(`43?^2f5~fqHPOch zuv*o>w0sfve4w`bVxdWVzpO9#nYU79-a%$lpBC?4UulcSpzhVp$?x{s5#3C5`+S%7 zS^IqLFR#zn-igoGcDJ&km&WF63t}_njSammz)t=Jei8&m()B5a1yfw1yGQg53|uSE z!vh1~mgk{?fvbGc@8N^w?hyyU{>k7Q$ywgMS7~~38XY|>FPHFgNM6QzqZ3Aq-nv4> z@9oeI)0nZ)-C7^L@b(39C$$Y%1by3*15qPT#x6G>!f~uWT89awV41)2uy#+QU{!%p zeHTQ5UoHi98XtdUL7><#&>FcC0AfwpfpsL;r+nk?26Yk7l`;SP`ezW5WzFtxbaA8p zy@3*8ul$+@$!ZK$Lm>Tx0Fn0uR3x^tAv!rcL)FM%MbvNs2Y~u8n;%(h(ly$05F`GN zSqX|D1_30-5F-`+}5hL~g}*0pH&DC#m2 z9Cy?P%#a3TY8NF?hy7~oi;QqttF zVDXCelG!*|MFz3&AN6o<@iW2x3M{b1pAcH1)?UKC{om?cSzQ@NSy+2@W9{A@$00Xk&@G|l+2L=xWMaX^U4`B;>bH9F%pt|$ulF454e%a`7nLS&)%>sBxQ__ zMFwp&vkkM#FTtl{y{ZaDg()6IWS(IWndK0fYA;>sHgq@0^^7L>XQY+0`F?_pe%+k_ z4|f3%t5{KtmlA-<_+_xH)e4w&!<-o;tTyH@o>sB$t`Me(maId{rYEFYEo{+c*um#p)>*&hyhZEmPQWrk(RU@;G? z5SKD=gyKBMl_eM+@)zhGaig$&R7uP<{{Otxl)OgA+N_wZT~Z!5O3JkoG?=WMllaI{ z5^^bFU(@pHd2!>|@&P62f2lkr%GRvgVU{fU3njlaMj54T!amic(95Bw%SNI!rA8av zRbco;#2{Sv|9z|@KRjZr?<6WS)(Z%!vHnh0PUC8c{TE{``Zn_fSmw!rN^|CE&gn=c zeya@o;XsC8WaeWa;O;B6Va+nYk;6}T;7i5x*>2LQJeS{Z6I-I*SOZVTLkL}gIRZG_-wl^WM( zm!BX!UKX?*OLf;?Ryv?~?T3xe><~2Xnq16Zb@a z93zdnExisR=h7i4?cdu)jGcR(+{}uwkj$JUe$FG}c&vYhbW2WVON}@h`=>*hwDYA+ zYj~c&#$(vb62eapiewGUQ2?z%0B#O~uKpT;PH0dx*<|jg`{sLm!wHvJ<$~%^1#6^%L>RggNVo68gbm3`#m!exwn(t|DwTaAh3n!X}edA zqLghC8f6xt!D^zNY{T_i3{ufsi8ySz6{Q29+~b&MN&P9j6QXK}@|aPSqq}DlzS0a2Zih+ghU=f3AUiQEm_!U9p zQar;dn(G{?Pk2f04$z8Q%}3^ZpY_=fgli@;=v@cN-_EPFeC%{-C@uf1X}C|-P(+#V z^MiLp95YDHAL{)xWGD(P0Taz(^(asR|4`-6cufAn_xKDd8gpRNj$yONde z0j}n0^7&Y{?Y!XoF$=Y~aY1Ly%TQf%E`?WtKXN9NH{&bvs=xDT639dNM4d22C~-pI^TLCSJikTWBu>|MOJl;YuyPFi~0$OR^y~>d~}GB6D<@a z!xVM=Xd|^?eM@cympg{>p=+I>5~X1y9O=HD(r#@HXH-SO(_o?CF(BHF?!^*Xx39@9 zJwX;hdeSY+mu6>8ht{b4)Bl`3jX7p*6(LF?_Wx+q`hD!GTa7nprtIlQ+}{l37>!8A zcpmF7B$H7Lw8$a&wytLPfF`%W)suPFllnR9Q9oxr^lh9kN=DAd$3Mik*UGoI%>vKm zd<&CS&WSP$d}~XJIq;kRpsKu+$NJ;tv!4M&s-(A{R4Iimq?{!wcc)T@5=Ap7NXj=P z<$9HpgI{v(j*dpT1{XwF$hO%%%k%VjcuziH+yb{5YzWspI{XYuQ%F24m#Ag3@>$I= zAAop8CW^YXoIGvG`3x)!A`i3gxB}@*a7A!}Yu#|tD0H3R9|}A~mkdMJ4sxV&Smcsp zQ2Pc20dZMR%Lkn(A4mM_`Z@$^8xK&rax9#>ABMLPDN6Y&6e}nqe?=ADPucJFIFp0D z5zRQ`B;@i;eNql`CYWAT0qdT^%)PUmH(%toZq|R5vHS#CqT69sA06|+O?ak%Vrq$= z7I8e>KTVEVZ7D{ATAXz(&O7>NvlgeT*{xP(9ZgbFOS1l8PB#2O)*r{*#(Nn=H5g^A zi-@D5e?e&!d3MBc%ux~rmq2-lPZ3wEdz?KX);%MRt}ps^IH%o-Ka+_S!4A+M4M@Do zA9F*1%u?w3I}yht{mV$V1$4daD2b{qfiqP*l4TLcxBKVGX@pGXRm={P*+8WN89-S^ zyu#ewibM$Vo}R;f@D1T7YK=P&F>+}Gs2NX70Ne8`fxzR&$TB1}a$KuQb2o~5K+0ztQ~=*940#bxIZlYXq3UWCc~I;3sD(x zOz~eTEQXMhtqLzKwQ*@Dl2tF=nueH8<3$1pBqZ5&ve&dFGIgTya3*s%JF_W#pc5(k zvwgV<$&Yd2=-3xO0Y3!fe$g8~gjgQymAjgo_|+lqGgp}d^XVUo(DdoK_mv_` zBJ!gc2orla;mwTv?qEWE*B)!EW$>)4vM!^Afbm!+#xQai~z z%RQ~J^H>4?bNACMv0L|5vFziz<3qxdeC9Bg6235z&uSDy&B+(60n5RJcdS+z>?b_d zpDrU)g&!4#6(3qb==1YroaS&`C&YX|Q6A#SaUB!oPIYRMvw76L${b_I`A{mn%E<|+ zRwt)re^5~h)d5Zv;&_3}98wjkQ$U}E>df%)Th&(y#c6qaf;1vCT_${?tEx&fn`D|< zh&YDO`<(qdw9ZR5o7Q+OcsAnL`|%O!9!VGOz+ZFIM!+42l#vGNIl#fz7;A?+5SHw4 z2Uh21R@?!}3}F0~pZimpvc}o=AO-JlfQQH`bZI z1!Uot;f=|X6_an+K~mDD{Wm~4qr{BR-oxzb19L0SYF*>wOhUvVoPwNjV#}_eM&z$1 zs0{5#$xx%()Oacx#GSOTO!XjzJ<>mKdeDccvhF@IT@5<;AR6od-wQN!$?ZV5+zxak zKBJGc0!Q}W4YG9^oL zE6<)1WLp`~LRn7{$E>5|lbHd8|G{4lOkEMjSx01Q3#sX2p?e~s8p#Ndj6Q-&8WnMT zn#dUi8WL@i4-B_z=j;8GVac!whXiIldW+?ka0s(_<11_`WOlp!g$IC~NxdsEwMk59i zMy5#x*`k*~Dqsb%p0AbGh)IGd>6y2T12_Cj<(qvd-%K_3UeAeJi~-0!?7yLWvya{3 z=X_(`E_50{At0N^$;LgYXyhwE>!O9DWDOIylOQ`|I!1JJEdI1PRzd^HAf5SeYLM^< z(cch6a;Z9)qg*6Z^{M~5ccyC5P-^~9dMBzy&wnS~yHq#kCY~TN)dkaPQTjf(g~3m4 zLMf5A*W279h_=mdbDx8W4!45LHg_5KAcV>#2|~6=o^9?@6c=+l7*i`(s(un7%kJ4NFUk^edbZ_bPqbu;iWG#=L7Qu-R5SgC{;^hc!ChRo_#B zxtOQ&0##2HpwJgQFIh|cDa7X!kFAMRj(o(LUoL3cE}4pxqei1v=LnRB_7o!Q9B?Af z319;r23xo-IXDyklyi|>_sL*d*L~@eg!z&~53$eEpwy_Fx9}op7>&-O;YLW^qiv9P zuIhhN8p}9wC$3{Wvitns z(!AV0zj>cj^{CnB4bo>h+Kn)7P8Bc0Lf@C-Ri77ALUx}$`m;y&852M9)j4batv)Nn z*<)NHXF{{jatKp>-q5elJLaYrdZ)3CfH}Y-Q2Ohi@_BfJ3@NnJ8fKhcsis9X$aKh| z>v6gvN2xbSZEie^*rvwMsYqL^rUKJIYQz;ieRFnI^9VFIt|x0#;|}u!8EQW0;2~$f z#!~^n#5eepJ>J#ei+-90n*}4;nQ&tN`@4%9P;O2M&dj-9o?Y)Q`=4z^ zCf}~GdQZ=tp2&j!y(j%Y?Y}50e0~30t<4W^s$gf)c9|ctZ>;8|I zZ~x!-|1!irzrOz$TB%3x|8Ohy-|qj1U)%q)Sm6J<|9|9LPeg6<#&K|cfdt{}9f36P zjTMyyBmRk6_d-sP`B5PfVDPQr;fZW`SP)gi<*d=r1|c+%x%CnNGb|u<6wwK-;ds)s z#BRA(;GA=Xg|VTIOfv?NMn#R7PBT;c`;eNVgs|gjb9QQl!jc-PqITIRcL)h_yn)kD zwQEH!S|Q%-`l?B7J=$7VFFTH4+uF#pV`NXW2#UxCDOnvSj5jU^S?R4`zr8i9EiYB1 z+VQMXX$YeuMPV!cX3D_~M$P?Gkt$fUll{}8X=|I76*a45Wj;V9i3NdLeMyP1HcdX{>vOVdYbRh~ zg`>iPU=-uoWCnP4nIHUX;AxN#!ipBP1#YxpWZ}2J9zH~3FQYVhZl;AIeReXzjAYSq zgp6Y{Il7Gk0zyX%d7Q%}O`AGt7y`Va@TE4cB$^i4WF~_vyRy(d##i5xjA<$y`IwAm zs*?7aEyLv~e~Smy9MYe|J6Fo*2q>$x&P4;VgJ zR%=c9pfbhmeeO=lP?jnIL$=bXj6GvB-A4L4dyOqbC6Yy*Pjl}j@ts&d?vTyK{wcJX zu?x$4DIw`|h=B3L6(DK|a5*q+J+l=O+Qj{{jM)DE9f~Bzg6cCVFvcTSN31URWQU?76A>;l`jvk|l zz){&KMaHtX5BJga^u?E&!(T8j=H+vDRSu{rm=k#~EK=`D0U+}M09GlEy1F=e^Eo)@ zea0)Q*4pwih6T)k@k_??fD3}qDUB1DsS-v|7W`(3Dh@(aQSM$%lgpN*;t7mdL0VE# zjK!I@){6kn7rMaee0nrtaK#X$vT&p!XA zjHlHzrV^@1dM8~QOHz6{m?QKb$iw~SrvgEi$l$OuOwI-(gQ=mm#y@4|N-Cm*|9JJ=MJ)ZA!1$LqdYFpRX5bmdQaA{lH(&E_a9zp6ao53By}v3q*p(*8YSg07AyT!MP?{ zmetR9=IyLu9ESj%iPqBkl?$ZH3rw3eD5d)%R}^8vIxyX*?+`~ezUcJsT77FgQ6RQw z_6!J2@J1mE7@qxqhw#qOi}_so#GJTZ4t|OLcmXcSF(tfUQE77K=-o(wR@j2Rx zTg5LN=d*9hp*QodD)QuCRm!h|-#C6Aepm9lh2Nc?a3X}y;ozI9tiWl?%63eq0WG*C z#a=Nw&*(Zl_T7sLJ<*_%#1VPWkejOWjEfli6y`A|m=h61e;9*fZSI(b-dc<+eKKeY z{n2ItL2>Hd=I)^yrdz^DQFANcoQ3G@6=g^qhu#!b!Nb!(M`A{8_j5Tm=toiA89|g!)OZhUBZEo8IW{c$B zual&iBUMrtjbv_UXw>oT87fMa&p7i#b=hs9`lhqbDerl1{utPc>8C=tZK40HdXJRek`aT<62nz)-I)U*o(0eSQ za3#5;^Z`J%J@ue>apswYYE?!{CR|B(E2CsNsFo~gk??eOQ0?=AYS(i>wl&#nLLW}5 z3B4YwX}WtjR!7vu`4b#L6!wS~Fe+@~`Cx}R_=V~;Zsw4XH3q_Dh4(|x=+a0UnZ}p% zWN_z+i)Cj0>*bO6{+N$b`1irGufe|xzgZ<($c<$_`KVY+P+JCb?eS7Rbk$fvL-2dM zfp-uj@8Wf8tg&mlfG?~M=>$>%y9vu$9CkF%Rot$-ZUmlER@UG5n8208j}3o(r9#At z6T1^Dd4Q!M%T!r?0jr}42o2fDnM0}+zd;1Z2% zWMbZ@3`@Q$IzJCv8t381{q0&k0gr@LUf(MYF3}Mso3|SIuZJmt9Iw{*_U$HNd6O9~ zDc4_yseMqq_}Ts@U)(NLgPMw4wJ+oCGz zo^O1xTWc5y2+0skYdYs5yiB52+mrY&>37cgUcR#xrPj@GS4-^aQPfzVwiiXCB+qfx z|HIbJ0K`8jafM#(s%^ThKsAnh4n>qG^28iZy^}>()=^s&YZ?%GZ&GMmsOg$PID#>4 z`Tt7v$5FcVMaPVm2B|566{J=~w=ri9qbG8t)dzk02U_EG%oq1mT>u<=7e0lcWYuT- zc+d$lEsLDV3J6dv#)!uk!?+wMN)z|92=HZBN_NgvILU|vnCmOs7g~nn4>X-;6lF^5 zF$PI#KtJL>bqe(|(*jl4%XmjNQScEWe)IeW-PWc)|2sT{l#3qTFM^&O&I&(k^ZBmt_235pUT3Io56s9|sIJQv zC~Bo*jBsAwJY#lfPeCk^hr3B-VAFBi@1z~W4?N6Kp&=#*Ftg!PaJpEk$D90J%DvSH z=D8D*QMyz^mxg;DPZ5S>6Jqqr)ZJ!=X14Z2PZ|KY(#$vvn%^uX45rU%0D!hP36)m2 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 ieUbal?F-vK7U{MiWlndSmg}W8fo72K