diff --git a/build/gulpfile.vscode.win32.js b/build/gulpfile.vscode.win32.js index 8630284bc1f63..15459f7dae80d 100644 --- a/build/gulpfile.vscode.win32.js +++ b/build/gulpfile.vscode.win32.js @@ -13,6 +13,7 @@ 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}`); @@ -77,7 +78,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) @@ -90,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/common/publish.ts b/build/tfs/common/publish.ts index e4cbdc80f0e7b..ca580ce1b4d05 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,13 @@ 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; + } + + console.log('Asset:', JSON.stringify(asset, null, ' ')); + const release = { id: commit, timestamp: (new Date()).getTime(), diff --git a/build/tfs/win32/1_build.ps1 b/build/tfs/win32/1_build.ps1 index bc6ade13de33b..0fcb56c1b9e4a 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 } # } diff --git a/build/win32/code.iss b/build/win32/code.iss index f4374ee099d51..1601f540fa2c3 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"; 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}\_" +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: "*"; 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}" @@ -71,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] @@ -955,6 +960,62 @@ begin Result := not WizardSilent(); end; +// Updates +function IsBackgroundUpdate(): Boolean; +begin + Result := ExpandConstant('{param:update|false}') <> 'false'; +end; + +function IsNotUpdate(): Boolean; +begin + Result := not IsBackgroundUpdate(); +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}')) + else + Result := True; +end; + +function GetAppMutex(Value: string): string; +begin + if IsBackgroundUpdate() then + Result := '' + else + Result := '{#AppMutex}'; +end; + +function GetDestDir(Value: string): string; +begin + if IsBackgroundUpdate() then + Result := ExpandConstant('{app}\_') + else + Result := ExpandConstant('{app}'); +end; + +procedure CurStepChanged(CurStep: TSetupStep); +var + UpdateResultCode: Integer; +begin + if IsBackgroundUpdate() and (CurStep = ssPostInstall) then + begin + CreateMutex('{#AppMutex}-ready'); + + while (CheckForMutexes('{#AppMutex}')) do + begin + Log('Application is still running, waiting'); + Sleep(1000); + end; + + Exec(ExpandConstant('{app}\inno_updater.exe'), ExpandConstant('--apply-update _ "{app}\unins000.dat"'), '', SW_SHOW, ewWaitUntilTerminated, UpdateResultCode); + 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 0000000000000..d82b1430c6407 Binary files /dev/null and b/build/win32/inno_updater.exe differ diff --git a/extensions/configuration-editing/src/extension.ts b/extensions/configuration-editing/src/extension.ts index aacb21ff77733..a081f0ad448f4 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/extensions/typescript/src/features/quickFixProvider.ts b/extensions/typescript/src/features/quickFixProvider.ts index 4c4a86d1d97e7..d83c23d64da80 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 14dca20a4b52b..088d887905912 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 []; } @@ -140,24 +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] - } - }); + 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] - } - }); + command: ApplyRefactoringCommand.ID, + arguments: [document, file, info.name, action.name, range] + }; + actions.push(codeAction); } } } @@ -166,4 +168,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/package.json b/package.json index 3312dc66b2d17..dc5b1d8acfcdb 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" }, @@ -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-beta7", "yauzl": "2.8.0" }, "devDependencies": { diff --git a/src/typings/iconv-lite.d.ts b/src/typings/iconv-lite.d.ts index 84c54e320cf37..5ad19bb95b7c3 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/typings/node.d.ts b/src/typings/node.d.ts index 37db4dd80f0ef..4fa1842198274 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/typings/windows-mutex.ts b/src/typings/windows-mutex.ts index 039dffcc2ad56..a3acc8f4430ce 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/base/browser/ui/list/listPaging.ts b/src/vs/base/browser/ui/list/listPaging.ts index 0c72f6ff5e320..2de68902c7cb1 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 1ee43f1916c3c..d1c7b58200854 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/base/browser/ui/resourceviewer/resourceViewer.ts b/src/vs/base/browser/ui/resourceviewer/resourceViewer.ts index 9f1e41502d2f5..86ac9fbeee483 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,30 +79,39 @@ export interface IResourceDescriptor { mime: string; } -// 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 */); - } +enum ScaleDirection { + IN, OUT, +} - const src = descriptor.resource.toString(); +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; - let cached = IMAGE_RESOURCE_ETAG_CACHE.get(src); - if (!cached) { - cached = { etag: descriptor.etag, src }; - IMAGE_RESOURCE_ETAG_CACHE.set(src, cached); - } + public static formatSize(size: number): string { + if (size < BinarySize.KB) { + return nls.localize('sizeB', "{0}B", size); + } - if (cached.etag !== descriptor.etag) { - cached.etag = descriptor.etag; - cached.src = `${src}?${Date.now()}`; // bypass cache with this trick + 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)); } +} - return cached.src; +export interface ResourceViewerContext { + layout(dimension: Dimension); } /** @@ -109,26 +119,30 @@ function imageSrc(descriptor: IResourceDescriptor): string { * 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 - public static show( descriptor: IResourceDescriptor, container: Builder, scrollbar: DomScrollableElement, openExternal: (uri: URI) => void, - metadataClb?: (meta: string) => void - ): 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()); @@ -136,72 +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)) { - $(container) - .empty() - .addClass('image') - .img({ src: imageSrc(descriptor) }) - .on(DOM.EventType.LOAD, (e, img) => { - const imgElement = img.getHTMLElement(); - if (imgElement.naturalWidth > imgElement.width || imgElement.naturalHeight > imgElement.height) { - $(container).addClass('oversized'); - - img.on(DOM.EventType.CLICK, (e, img) => { - $(container).toggleClass('full-size'); - - scrollbar.scanDomNode(); - }); - } - - if (metadataClb) { - metadataClb(nls.localize('imgMeta', "{0}x{1} {2}", imgElement.naturalWidth, imgElement.naturalHeight, ResourceViewer.formatSize(descriptor.size))); - } - - scrollbar.scanDomNode(); - }); - } else { - const imageContainer = $(container) - .empty() - .p({ - text: nls.localize('largeImageError', "The image is too large to display in the editor. ") - }); +class ImageView { + private static readonly MAX_IMAGE_SIZE = BinarySize.MB; // showing images inline is memory intense, so we have a limit - 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); - })); - } - } + 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); } - // 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(); - } + LargeImageView.create(container, descriptor, openExternal); + return null; } - private static inlineImage(descriptor: IResourceDescriptor): boolean { + private static shouldShowImageInline(descriptor: IResourceDescriptor): boolean { let skipInlineImage: boolean; // Data URI @@ -210,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); + })); } + } +} + +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(); + } +} - if (size < ResourceViewer.MB) { - return nls.localize('sizeKB', "{0}KB", (size / ResourceViewer.KB).toFixed(2)); +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 */); } - if (size < ResourceViewer.GB) { - return nls.localize('sizeMB', "{0}MB", (size / ResourceViewer.MB).toFixed(2)); + 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 (size < ResourceViewer.TB) { - return nls.localize('sizeGB', "{0}GB", (size / ResourceViewer.GB).toFixed(2)); + if (cached.etag !== descriptor.etag) { + cached.etag = descriptor.etag; + cached.src = `${src}?${Date.now()}`; // bypass cache with this trick } - return nls.localize('sizeTB', "{0}TB", (size / ResourceViewer.TB).toFixed(2)); + return cached.src; } } diff --git a/src/vs/base/browser/ui/resourceviewer/resourceviewer.css b/src/vs/base/browser/ui/resourceviewer/resourceviewer.css index c4badd1d2c5cb..054159a1281ed 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,25 @@ 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.pixelated { + image-rendering: pixelated; +} + +.monaco-resource-viewer img.untouched { max-width: 100%; - max-height: calc(100% - 10px); /* somehow this prevents scrollbars from showing up */ + object-fit: contain; + image-rendering: auto; +} + +.monaco-resource-viewer img { + margin: auto; /* centers the image */ } -.monaco-resource-viewer.oversized img { +.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/base/node/encoding.ts b/src/vs/base/node/encoding.ts index 4177f3ffab5c5..1d134c6576791 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 b3a4121079efc..963a9a3905e28 100644 --- a/src/vs/base/node/extfs.ts +++ b/src/vs/base/node/extfs.ts @@ -54,7 +54,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 +75,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 +120,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 +135,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 +319,101 @@ export function mv(source: string, target: string, callback: (error: Error) => v }); } +let canFlush = true; +export function writeFileAndFlush(path: string, data: string | NodeBuffer | NodeJS.ReadableStream, options: { mode?: number; flag?: string; }, callback: (error?: Error) => void): void { + options = ensureOptions(options); + + if (typeof data === 'string' || Buffer.isBuffer(data)) { + doWriteFileAndFlush(path, data, options, callback); + } else { + doWriteFileStreamAndFlush(path, data, options, callback); + } +} + +function doWriteFileStreamAndFlush(path: string, reader: NodeJS.ReadableStream, 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. 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 }); + + // 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 => { + fd = descriptor; + isOpen = true; + }); + + // 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)); + + // 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) { + 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.close(); + }); + } else { + writer.close(); + } + }); + + // 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 // 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 b949384de1ed1..9ebc819fd0236 100644 --- a/src/vs/base/node/pfs.ts +++ b/src/vs/base/node/pfs.ts @@ -101,6 +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: 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 06ecf0b8ba0b1..f521abbfd01b9 100644 --- a/src/vs/base/test/node/extfs/extfs.test.ts +++ b/src/vs/base/test/node/extfs/extfs.test.ts @@ -15,6 +15,8 @@ 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'; +import { isLinux } from 'vs/base/common/platform'; const ignore = () => { }; @@ -22,6 +24,38 @@ 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); + } + }, + encoding: 'utf8' + }); +} + suite('Extfs', () => { test('mkdirp', function (done: () => void) { @@ -174,7 +208,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 +243,196 @@ 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('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); + 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); diff --git a/src/vs/code/electron-browser/issue/issueReporter.html b/src/vs/code/electron-browser/issue/issueReporter.html index 553c3d51de595..ad3010252ec57 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 f37957e151807..0b8148580cc2c 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'; @@ -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) { @@ -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 { @@ -246,9 +255,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 +265,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.'; } @@ -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; } /** diff --git a/src/vs/code/electron-browser/issue/media/issueReporter.css b/src/vs/code/electron-browser/issue/media/issueReporter.css index cb43afc9bc1f8..e4ca54e35b53b 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; diff --git a/src/vs/code/electron-browser/sharedProcess/contrib/languagePackExtensions.ts b/src/vs/code/electron-browser/sharedProcess/contrib/languagePackExtensions.ts index 1add53f47773f..64fd839576361 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/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index 46fbe9ce0b2fd..24fb6d6ca51d8 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'; import { IIssueService } from 'vs/platform/issue/common/issue'; import { IssueChannel } from 'vs/platform/issue/common/issueIpc'; import { IssueService } from 'vs/platform/issue/electron-main/issueService'; @@ -307,7 +309,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/logUploader.ts b/src/vs/code/electron-main/logUploader.ts index f4a034627d327..bbcc8a6a76a62 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) { @@ -94,12 +93,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( diff --git a/src/vs/code/electron-main/main.ts b/src/vs/code/electron-main/main.ts index ea7bf7334067d..8ba25df359bcb 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())); } diff --git a/src/vs/code/electron-main/menus.ts b/src/vs/code/electron-main/menus.ts index aafae0c8ed99c..7e389c637293a 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,45 +1040,51 @@ 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('miRestartToUpdate', "Restart to Update..."), click: () => { - this.reportMenuActionTelemetry('RestartToUpdate'); - this.updateService.quitAndInstall(); - } + label: nls.localize('miCheckForUpdates', "Check for Updates..."), click: () => setTimeout(() => { + this.reportMenuActionTelemetry('CheckForUpdate'); + this.updateService.checkForUpdates(true); + }, 0) })]; - case UpdateState.CheckingForUpdate: + case StateType.CheckingForUpdates: 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..."); + case StateType.AvailableForDownload: + return [new MenuItem({ + label: nls.localize('miDownloadUpdate', "Download Available Update"), click: () => { + this.updateService.downloadUpdate(); + } + })]; - return [new MenuItem({ label: updateAvailableLabel, enabled: false })]; + case StateType.Downloading: + return [new MenuItem({ label: nls.localize('miDownloadingUpdate', "Downloading Update..."), enabled: false })]; - default: - const result = [new MenuItem({ - label: nls.localize('miCheckForUpdates', "Check for Updates..."), click: () => setTimeout(() => { - this.reportMenuActionTelemetry('CheckForUpdate'); - this.updateService.checkForUpdates(true); - }, 0) + case StateType.Downloaded: + return [new MenuItem({ + label: nls.localize('miInstallUpdate', "Install Update..."), click: () => { + this.reportMenuActionTelemetry('InstallUpdate'); + this.updateService.applyUpdate(); + } })]; - return result; + case StateType.Updating: + return [new MenuItem({ label: nls.localize('miInstallingUpdate', "Installing Update..."), enabled: false })]; + + case StateType.Ready: + return [new MenuItem({ + label: nls.localize('miRestartToUpdate', "Restart to Update..."), click: () => { + this.reportMenuActionTelemetry('RestartToUpdate'); + this.updateService.quitAndInstall(); + } + })]; } } diff --git a/src/vs/code/electron-main/windows.ts b/src/vs/code/electron-main/windows.ts index 073f7dc441ff4..8ecf849991023 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 { diff --git a/src/vs/editor/browser/viewParts/lines/viewLine.ts b/src/vs/editor/browser/viewParts/lines/viewLine.ts index 5f83e8df9899c..7a9761d20d8f1 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, @@ -278,8 +281,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 +347,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 +437,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); diff --git a/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts b/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts index 9c4b974dd112a..3c98e8b6b31ca 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; } @@ -189,7 +202,6 @@ export class PieceTreeBase { protected _lineCnt: number; protected _length: number; private _lastChangeBufferPos: BufferCursor; - private _lastNodePosition: NodePosition; constructor(chunks: StringBuffer[]) { this.create(chunks); @@ -200,7 +212,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; @@ -277,6 +288,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); @@ -437,7 +451,6 @@ export class PieceTreeBase { // changed buffer this.appendToNode(node, value); this.computeBufferMetadata(); - this._lastNodePosition = { node, remainder, nodeStartOffset }; return; } @@ -921,33 +934,7 @@ export class PieceTreeBase { 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 - }; - } - nodeAt(offset: number): NodePosition { - let cachedNodePosition = this.readNodePositionFromCache(offset); - if (cachedNodePosition) { - return cachedNodePosition; - } - let x = this.root; let nodeStartOffset = 0; diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index 90e95ac7bb7f4..5a05edb5ccc1f 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() { @@ -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/editor/common/model/textModelSearch.ts b/src/vs/editor/common/model/textModelSearch.ts index aa969b5334a7b..2f1526e618e5a 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); diff --git a/src/vs/editor/common/modes.ts b/src/vs/editor/common/modes.ts index c3c19319c4854..6b573d085a9c2 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/goToDeclaration/goToDeclaration.ts b/src/vs/editor/contrib/goToDeclaration/goToDeclaration.ts index b3ecb2c6d4bee..f79912d832315 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)); } diff --git a/src/vs/editor/contrib/quickFix/codeActionTrigger.ts b/src/vs/editor/contrib/quickFix/codeActionTrigger.ts new file mode 100644 index 0000000000000..a210e4844460e --- /dev/null +++ b/src/vs/editor/contrib/quickFix/codeActionTrigger.ts @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * 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(''); + public static readonly Refactor = new CodeActionKind('refactor'); + + 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 8e619e15c03e6..10bbbe4d5e891 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 2d067db0bf0ec..15f8c5ee8d868 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,87 @@ 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); + } + } +} + + +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()); diff --git a/src/vs/editor/contrib/quickFix/quickFixModel.ts b/src/vs/editor/contrib/quickFix/quickFixModel.ts index e2ff0dcd2a1e0..3e4366c4d633e 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 8e1fdc79473b7..ba52da9dde7c2 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 c2806d7d31ed2..a6c3b4663a766 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/contrib/suggest/suggestWidget.ts b/src/vs/editor/contrib/suggest/suggestWidget.ts index 6624f1ca547d0..05c83cead70f8 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; diff --git a/src/vs/editor/standalone/browser/standaloneLanguages.ts b/src/vs/editor/standalone/browser/standaloneLanguages.ts index 5c5b72caa39f9..1e4f3f4fc2993 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/editor/test/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.test.ts b/src/vs/editor/test/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.test.ts index 83eaf75bde018..a396f91cc406c 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,76 @@ 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)); + }); +}); + +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 diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index b419e7e58ba4d..dd87b203a1760 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/platform/contextkey/common/contextkey.ts b/src/vs/platform/contextkey/common/contextkey.ts index 69381904e0a7e..5461a40cf0705 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 69f8bca09302b..4cdda5588c52d 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); diff --git a/src/vs/platform/extensionManagement/common/extensionEnablementService.ts b/src/vs/platform/extensionManagement/common/extensionEnablementService.ts index 990894ef86203..8e4c9917668dc 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 28f19aa05dff7..9789db5e966d8 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 4ccb66912293b..efdb210e498f3 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/platform/files/common/files.ts b/src/vs/platform/files/common/files.ts index af89c431ab687..0fe4bf63718e2 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/platform/list/browser/listService.ts b/src/vs/platform/list/browser/listService.ts index 1bba7f047a698..d283b200549ef 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/platform/update/common/update.ts b/src/vs/platform/update/common/update.ts index 13928e33f1784..8ce018cb411f7 100644 --- a/src/vs/platform/update/common/update.ts +++ b/src/vs/platform/update/common/update.ts @@ -9,35 +9,71 @@ 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 -} - -export enum ExplicitState { - Implicit, - Explicit -} - -export interface IRawUpdate { - releaseNotes: string; - version: string; - date: Date; -} - export interface IUpdate { version: string; + productVersion: string; date?: Date; releaseNotes?: string; + supportsFastUpdate?: boolean; url?: string; + 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', + CheckingForUpdates = 'checking for updates', + AvailableForDownload = 'available for download', + 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 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 | 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), + 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), + Ready: (update: IUpdate) => ({ type: StateType.Ready, update } as Ready), +}; + export interface IAutoUpdater extends NodeEventEmitter { setFeedURL(url: string): void; checkForUpdates(): void; + applyUpdate?(): TPromise; quitAndInstall(): void; } @@ -46,13 +82,11 @@ export const IUpdateService = createDecorator('updateService'); export interface IUpdateService { _serviceBrand: any; - readonly onError: Event; - readonly onUpdateAvailable: Event<{ url: string; version: string; }>; - readonly onUpdateNotAvailable: Event; - readonly onUpdateReady: Event; readonly onStateChange: Event; readonly state: State; - checkForUpdates(explicit: boolean): TPromise; + 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 bd42c38e018cf..441bdd138e139 100644 --- a/src/vs/platform/update/common/updateIpc.ts +++ b/src/vs/platform/update/common/updateIpc.ts @@ -9,15 +9,12 @@ 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:onUpdateReady'): TPromise; - call(command: 'event:onStateChange'): TPromise; - call(command: 'checkForUpdates', arg: boolean): TPromise; + call(command: 'checkForUpdates', arg: boolean): TPromise; + call(command: 'downloadUpdate'): TPromise; + call(command: 'applyUpdate'): TPromise; call(command: 'quitAndInstall'): TPromise; call(command: '_getInitialState'): TPromise; call(command: string, arg?: any): TPromise; @@ -29,12 +26,10 @@ 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:onUpdateReady': return eventToCall(this.service.onUpdateReady); 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); } @@ -46,19 +41,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 _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; } @@ -78,10 +62,18 @@ export class UpdateChannelClient implements IUpdateService { }, onUnexpectedError); } - checkForUpdates(explicit: boolean): TPromise { + checkForUpdates(explicit: boolean): TPromise { return this.channel.call('checkForUpdates', explicit); } + downloadUpdate(): TPromise { + return this.channel.call('downloadUpdate'); + } + + applyUpdate(): TPromise { + return this.channel.call('applyUpdate'); + } + quitAndInstall(): TPromise { return this.channel.call('quitAndInstall'); } 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 0000000000000..04e3ccdfeb369 --- /dev/null +++ b/src/vs/platform/update/electron-main/abstractUpdateService.ts @@ -0,0 +1,162 @@ +/*--------------------------------------------------------------------------------------------- + * 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, AvailableForDownload } 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.logService.info('update#setState', state.type); + this._state = state; + this._onStateChange.fire(state); + } + + constructor( + @ILifecycleService private lifecycleService: ILifecycleService, + @IConfigurationService protected configurationService: IConfigurationService, + @IEnvironmentService private environmentService: IEnvironmentService, + @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; + } + + 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 { + this.logService.trace('update#checkForUpdates, state = ', this.state.type); + + if (this.state.type !== StateType.Idle) { + return TPromise.as(null); + } + + return this.throttler.queue(() => TPromise.as(this.doCheckForUpdates(explicit))); + } + + downloadUpdate(): TPromise { + this.logService.trace('update#downloadUpdate, state = ', this.state.type); + + if (this.state.type !== StateType.AvailableForDownload) { + return TPromise.as(null); + } + + return this.doDownloadUpdate(this.state); + } + + protected doDownloadUpdate(state: AvailableForDownload): TPromise { + return TPromise.as(null); + } + + applyUpdate(): TPromise { + this.logService.trace('update#applyUpdate, state = ', this.state.type); + + if (this.state.type !== StateType.Downloaded) { + return TPromise.as(null); + } + + return this.doApplyUpdate(); + } + + protected doApplyUpdate(): TPromise { + return TPromise.as(null); + } + + quitAndInstall(): TPromise { + this.logService.trace('update#quitAndInstall, state = ', this.state.type); + + 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 bdc9c183329c4..0000000000000 --- 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 c88880617f32f..0000000000000 --- a/src/vs/platform/update/electron-main/auto-updater.win32.ts +++ /dev/null @@ -1,139 +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 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; -} - -export class Win32AutoUpdaterImpl extends EventEmitter implements IAutoUpdater { - - private url: string = null; - private currentRequest: Promise = null; - private updatePackagePath: string = 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 => { - this.updatePackagePath = updatePackagePath; - - this.emit('update-downloaded', - {}, - update.releaseNotes, - update.productVersion, - new Date(), - this.url - ); - }); - }); - }) - .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)) - )) - ); - } - - quitAndInstall(): void { - if (!this.updatePackagePath) { - return; - } - - spawn(this.updatePackagePath, ['/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 0000000000000..88b0f19310221 --- /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 0000000000000..cb36c65858561 --- /dev/null +++ b/src/vs/platform/update/electron-main/updateService.linux.ts @@ -0,0 +1,84 @@ +/*--------------------------------------------------------------------------------------------- + * 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, 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'; +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 { + + _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.setState(State.CheckingForUpdates(explicit)); + + 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.AvailableForDownload(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 doDownloadUpdate(state: AvailableForDownload): TPromise { + shell.openExternal(state.update.url); + this.setState(State.Idle); + + return TPromise.as(null); + } +} 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 b7f8f5edf69a1..0000000000000 --- a/src/vs/platform/update/electron-main/updateService.ts +++ /dev/null @@ -1,295 +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 _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) => ({ releaseNotes, version, date })); - } - - get state(): State { - return this._state; - } - - set state(state: State) { - 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.state = 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.state = 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); - - this.raw.checkForUpdates(); - }).then(update => { - if (!update) { - this._onUpdateNotAvailable.fire(explicit); - this.state = State.Idle; - /* __GDPR__ - "update:notAvailable" : { - "explicit" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - } - */ - this.telemetryService.publicLog('update:notAvailable', { explicit }); - - } 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.state = 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 - }; - - this._availableUpdate = data; - this._onUpdateReady.fire(data); - this.state = State.UpdateDownloaded; - /* __GDPR__ - "update:downloaded" : { - "version" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - } - */ - this.telemetryService.publicLog('update:downloaded', { version: update.version }); - } - - return update; - }, err => { - this.state = 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; - } - - 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 0000000000000..ef20367626c46 --- /dev/null +++ b/src/vs/platform/update/electron-main/updateService.win32.ts @@ -0,0 +1,210 @@ +/*--------------------------------------------------------------------------------------------- + * 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 => { + const fastUpdatesEnabled = this.configurationService.getValue('update.enableWindowsBackgroundUpdates'); + + this.availableUpdate = { packagePath }; + + if (fastUpdatesEnabled && 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'], + windowsVerbatimArguments: true + }); + + 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/platform/workspace/common/workspace.ts b/src/vs/platform/workspace/common/workspace.ts index af594644baea5..1493935a0130b 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/vscode.d.ts b/src/vs/vscode.d.ts index 4f851542eff54..184c8b3f1106d 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; } /** @@ -1830,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; @@ -1853,6 +1920,13 @@ declare module 'vscode' { */ command?: Command; + /** + * Kind of the code action. + * + * Used to filter code actions. + */ + kind?: CodeActionKind; + /** * Creates a new code action. * @@ -1860,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/browser/localizationsExtensionPoint.ts b/src/vs/workbench/api/browser/localizationsExtensionPoint.ts new file mode 100644 index 0000000000000..cd92ed0e803d1 --- /dev/null +++ b/src/vs/workbench/api/browser/localizationsExtensionPoint.ts @@ -0,0 +1,70 @@ +/*--------------------------------------------------------------------------------------------- + * 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', + default: 'translations' + } + } + } + }; +} + +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/api/electron-browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/electron-browser/mainThreadLanguageFeatures.ts index d60034d69a219..1b6408a0ec8e7 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/electron-browser/mainThreadSaveParticipant.ts b/src/vs/workbench/api/electron-browser/mainThreadSaveParticipant.ts index 07a61e0f3b8dc..ad20179ae9bd9 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; } diff --git a/src/vs/workbench/api/node/extHost.api.impl.ts b/src/vs/workbench/api/node/extHost.api.impl.ts index 4d6eb3123576d..cc7aafe634b8b 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 ea9d6e8b96091..faacf07efa71a 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 { @@ -639,6 +640,7 @@ export interface CodeActionDto { edit?: WorkspaceEditDto; diagnostics?: IMarkerData[]; command?: modes.Command; + scope?: string; } export interface ExtHostLanguageFeaturesShape { @@ -651,7 +653,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 bf16fdb2c526e..f95fca70c4208 100644 --- a/src/vs/workbench/api/node/extHostApiCommands.ts +++ b/src/vs/workbench/api/node/extHostApiCommands.ts @@ -417,8 +417,11 @@ 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.edit) { + ret.edit = typeConverters.WorkspaceEdit.to(codeAction.edit); + } return ret; } }); diff --git a/src/vs/workbench/api/node/extHostExtensionService.ts b/src/vs/workbench/api/node/extHostExtensionService.ts index d39a58a432e2f..73a8df57d4e0a 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) ); diff --git a/src/vs/workbench/api/node/extHostLanguageFeatures.ts b/src/vs/workbench/api/node/extHostLanguageFeatures.ts index 37ac8015683b0..9ebfc2176560e 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 76c30af3a0f89..761ffee760804 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[]; - constructor(title: string, edit?: WorkspaceEdit) { + kind?: CodeActionKind; + + constructor(title: string, kind?: CodeActionKind) { this.title = title; - this.edit = edit; + this.kind = kind; } } + +export class CodeActionKind { + private static readonly sep = '.'; + + public static readonly Empty = new CodeActionKind(''); + 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'); + + 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/browser/actions.ts b/src/vs/workbench/browser/actions.ts index 12ac79bfc7832..e15e84abf60fa 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; diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts index 627be4bee3400..122d34883f6d0 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 6c4f3102f8464..61b5c8d569798 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/browser/parts/editor/binaryEditor.ts b/src/vs/workbench/browser/parts/editor/binaryEditor.ts index 282705c8e7417..8d4b64f9f4b55 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 +} diff --git a/src/vs/workbench/browser/parts/editor/editorCommands.ts b/src/vs/workbench/browser/parts/editor/editorCommands.ts index 42d2ed951c40f..f31156a4099a6 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/noTabsTitleControl.ts b/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts index 34cae60469c36..8d0c4ef497fb8 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 6ac7609d53164..892dc38d274eb 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 4c2fbffde2a90..a4dba3d6963e1 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'; @@ -170,6 +170,8 @@ export abstract class TitleControl extends Themable implements ITitleAreaControl public setContext(group: IEditorGroup): void { this.context = group; + + this.editorActionsToolbar.context = { groupId: group ? group.id : void 0 } as IEditorCommandsContext; } public hasContext(): boolean { @@ -396,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 03436fe0657df..96b57287a7110 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; diff --git a/src/vs/workbench/common/editor/textEditorModel.ts b/src/vs/workbench/common/editor/textEditorModel.ts index 524ffbdf8d6ec..8c10065dfcc6a 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'; @@ -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. @@ -90,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); @@ -105,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() || ''); } /** @@ -130,13 +141,10 @@ 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 { + public createSnapshot(): ITextSnapshot { const model = this.textEditorModel; if (model) { - return model.getValue(EndOfLinePreference.TextDefined, true /* Preserve BOM */); + return model.createSnapshot(true /* Preserve BOM */); } return null; diff --git a/src/vs/workbench/common/editor/untitledEditorModel.ts b/src/vs/workbench/common/editor/untitledEditorModel.ts index 9a817653721ae..c7dcee066bd45 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/electron-browser/main.contribution.ts b/src/vs/workbench/electron-browser/main.contribution.ts index 6bcbf87b95d1e..d72fb4e04e6d2 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 e3b64be9207f2..2b45ff41a7bff 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 e13edb24a9341..551bcc802f5fb 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); } diff --git a/src/vs/workbench/parts/backup/common/backupModelTracker.ts b/src/vs/workbench/parts/backup/common/backupModelTracker.ts index 7078c5cd4b565..1c61b9715e28e 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/extensions/browser/extensionsActions.ts b/src/vs/workbench/parts/extensions/browser/extensionsActions.ts index 905b22e3f8837..0b244b693db30 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/extensions/electron-browser/extensionTipsService.ts b/src/vs/workbench/parts/extensions/electron-browser/extensionTipsService.ts index 619aace599d43..e630ffa559aa3 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" }, diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensionsViews.ts b/src/vs/workbench/parts/extensions/electron-browser/extensionsViews.ts index df49559b53816..0369433172966 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) { 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 bad1b92e99441..8a9a44e97dfcd 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 23c2fa6549b00..bc300591ee478 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; diff --git a/src/vs/workbench/parts/files/electron-browser/fileActions.ts b/src/vs/workbench/parts/files/electron-browser/fileActions.ts index e80a6804eb1f0..952968f7641f2 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/saveErrorHandler.ts b/src/vs/workbench/parts/files/electron-browser/saveErrorHandler.ts index 379a2a53d81bc..3a0864e2ea7d5 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/parts/files/electron-browser/views/explorerViewer.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts index a812d54aaff8d..49550e507a130 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/parts/files/electron-browser/views/openEditorsView.ts b/src/vs/workbench/parts/files/electron-browser/views/openEditorsView.ts index 2c882a9221be5..d411f925e0916 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 }); } } 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 4b9590f037f44..a76038659ec0b 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/html/browser/webview.ts b/src/vs/workbench/parts/html/browser/webview.ts index 90f7cd006cd44..4010801932eaf 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)) { diff --git a/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts b/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts index 941892045d0e6..054e0266e428f 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 { @@ -364,11 +364,13 @@ class PreferencesRenderersController extends Disposable { private _editablePreferencesRendererDisposables: IDisposable[] = []; private _settingsNavigator: SettingsNavigator; - private _filtersInProgress: TPromise[]; + private _remoteFilterInProgress: 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; @@ -380,6 +382,10 @@ class PreferencesRenderersController extends Disposable { super(); } + get lastFilterResult(): IFilterOrSearchResult { + return this._lastFilterResult; + } + get defaultPreferencesRenderer(): IPreferencesRenderer { return this._defaultPreferencesRenderer; } @@ -413,34 +419,47 @@ 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._remoteFilterInProgress && this._remoteFilterInProgress.cancel) { + // Resolved/rejected promises have no .cancel() + this._remoteFilterInProgress.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._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 this._remoteFilterInProgress.then(() => { + this._remoteFilterInProgress = 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")); + return this.filterOrSearchPreferences(query, this._currentLocalSearchProvider, 'filterResult', nls.localize('filterResult', "Filtered Results"), 0); } - filterOrSearchPreferences(query: string, searchProvider: ISearchProvider, groupId: string, groupLabel: string): TPromise { + private filterOrSearchPreferences(query: string, searchProvider: ISearchProvider, groupId: string, groupLabel: string, groupOrder: number): TPromise { this._lastQuery = query; - if (this._filtersInProgress) { - // Resolved/rejected promises have no .cancel() - this._filtersInProgress.forEach(p => p.cancel && p.cancel()); - } - this._filtersInProgress = [ - this._filterOrSearchPreferences(query, this.defaultPreferencesRenderer, searchProvider, groupId, groupLabel), - this._filterOrSearchPreferences(query, this.editablePreferencesRenderer, searchProvider, groupId, groupLabel)]; + const filterPs = [ + this._filterOrSearchPreferences(query, this.defaultPreferencesRenderer, searchProvider, groupId, groupLabel, groupOrder), + this._filterOrSearchPreferences(query, this.editablePreferencesRenderer, searchProvider, groupId, groupLabel, groupOrder)]; - return TPromise.join(this._filtersInProgress).then(results => { - this._filtersInProgress = null; + return TPromise.join(filterPs).then(results => { const [defaultFilterResult, editableFilterResult] = results; this.consolidateAndUpdate(defaultFilterResult, editableFilterResult); @@ -449,13 +468,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; }); } @@ -469,7 +482,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); @@ -493,7 +506,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 6d2195213c134..b467aa4b539ae 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'; @@ -16,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, 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'; @@ -33,6 +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 { CodeLensProviderRegistry, CodeLensProvider, ICodeLensSymbol } from 'vs/editor/common/modes'; +import { CancellationToken } from 'vs/base/common/cancellation'; export interface IPreferencesRenderer extends IDisposable { readonly preferencesModel: IPreferencesEditorModel; @@ -254,6 +257,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 }>(); @@ -278,6 +282,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())); @@ -305,6 +310,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); @@ -313,6 +319,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); @@ -322,6 +329,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(); @@ -615,20 +623,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)); @@ -835,6 +846,51 @@ export class HighlightMatchesRenderer extends Disposable { } } +export class ExtensionCodelensRenderer extends Disposable implements CodeLensProvider { + private filterResult: IFilterResult; + + constructor() { + super(); + this._register(CodeLensProviderRegistry.register({ pattern: '**/settings.json' }, this)); + } + + public render(filterResult: IFilterResult): void { + this.filterResult = filterResult; + } + + public provideCodeLenses(model: ITextModel, token: CancellationToken): ICodeLensSymbol[] { + if (!this.filterResult || !this.filterResult.filteredGroups) { + return []; + } + + 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; + } +} + 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 9330e27ba9148..5157b9cefb83a 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; } @@ -56,6 +57,11 @@ export interface ISetting { overrideOf?: ISetting; } +export interface IExtensionSetting extends ISetting { + extensionName: string; + extensionPublisher: string; +} + export interface ISearchResult { filterMatches: ISettingMatch[]; metadata?: IFilterMetadata; @@ -65,6 +71,7 @@ export interface ISearchResultGroup { id: string; label: string; result: ISearchResult; + order: number; } export interface IFilterResult { @@ -82,7 +89,18 @@ export interface ISettingMatch { } export interface IScoredResults { - [key: string]: number; + [key: string]: IRemoteSetting; +} + +export interface IRemoteSetting { + score: number; + key: string; + id: string; + defaultValue: string; + description: string; + packageId: string; + extensionName?: string; + extensionPublisher?: string; } export interface IFilterMetadata { @@ -102,7 +120,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; @@ -177,7 +195,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/common/preferencesModels.ts b/src/vs/workbench/parts/preferences/common/preferencesModels.ts index 18b63f4e9c0a2..95c7d25596296 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[] { @@ -57,11 +59,11 @@ 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({ - setting, + setting: this.copySetting(setting), matches: settingMatchResult && settingMatchResult.matches, score: settingMatchResult ? settingMatchResult.score : 0 }); @@ -70,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 { @@ -86,6 +103,18 @@ export abstract class AbstractSettingsModel extends EditorModel { return null; } + private copySetting(setting: ISetting): ISetting { + return { + description: setting.description, + key: setting.key, + value: setting.value, + range: setting.range, + overrides: [], + overrideOf: setting.overrideOf + }; + } + + protected get filterGroups(): ISettingsGroup[] { return this.settingsGroups; } @@ -611,7 +640,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; @@ -665,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(','); @@ -718,19 +734,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, @@ -739,7 +742,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 763be7bcc0539..7614813e29fe7 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, 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'; @@ -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; @@ -67,8 +68,17 @@ 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): 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 { @@ -90,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, @@ -113,77 +123,124 @@ 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, endpoint: IEndpointDetails, private installedExtensions: TPromise, + constructor(private options: IRemoteSearchProviderOptions, private installedExtensions: TPromise, @IEnvironmentService private environmentService: IEnvironmentService, @IRequestService private requestService: IRequestService, + @ILogService private logService: ILogService ) { - this._filter = filter; - - // @queries are always handled by local filter - this._remoteSearchP = filter && !strings.startsWith(filter, '@') ? - this.getSettingsFromBing(filter, endpoint) : - 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 { 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.options.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 + }; + }); - const settingMatcher = this.getRemoteSettingMatcher(remoteResult.scoredResults, minScore, preferencesModel); - const filterMatches = preferencesModel.filterSettings(this._filter, group => null, settingMatcher); return { filterMatches, metadata: remoteResult }; } else { - return null; + const settingMatcher = this.getRemoteSettingMatcher(remoteResult.scoredResults, minScore, preferencesModel); + const filterMatches = preferencesModel.filterSettings(this.options.filter, group => null, settingMatcher); + return { + filterMatches, + metadata: remoteResult + }; } }); } - 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.prepareRequest(filter).then(details => { + this.logService.debug(`Searching settings via ${details.url}`); + if (details.body) { + this.logService.debug(`Body: ${details.body}`); + } + + const requestType = details.body ? 'post' : 'get'; return this.requestService.request({ - url, + type: requestType, + url: details.url, + data: details.body, headers: { 'User-Agent': 'request', 'Content-Type': 'application/json; charset=utf-8', - 'api-key': endpoint.key + 'api-key': this.options.endpoint.key }, 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); }).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 => { + const key = JSON.parse(r.setting || r.Setting); + const packageId = r['packageid']; + const id = getSettingKey(key, packageId); + + const packageName = r['packagename']; + let extensionName: string; + let extensionPublisher: string; + if (packageName && packageName.indexOf('##') >= 0) { + [extensionPublisher, extensionName] = packageName.split('##'); + } + + return { + key, + id, + defaultValue: r['value'], + score: r['@search.score'], + description: JSON.parse(r['details']), + packageId, + extensionName, + extensionPublisher + }; + }); const scoredResults = Object.create(null); - suggestions.forEach(s => { - const name = s.name - .replace(/^"/, '') - .replace(/"$/, ''); - scoredResults[name] = s.score; + remoteSettings.forEach(s => { + scoredResults[s.id] = s; }); return { - remoteUrl: url, + remoteUrl: details.url, // telemetry for filter text? duration, timestamp, scoredResults, @@ -194,18 +251,20 @@ 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 settingMatches = new SettingMatches(this._filter, setting, false, (filter, setting) => preferencesModel.findValueMatches(filter, setting)).matches; - return { matches: settingMatches, score: scoredResults[setting.key] }; + return (setting: ISetting, group: ISettingsGroup) => { + 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 }; } return null; }; } - private prepareUrl(query: string, endpoint: IEndpointDetails, buildNumber: number): TPromise { + private async prepareRequest(query: string): TPromise<{ url: string, body?: string }> { query = escapeSpecialChars(query); const boost = 10; const userQuery = `(${query})^${boost}`; @@ -214,41 +273,73 @@ export class RemoteSearchProvider implements ISearchProvider { query = query.replace(/\ +/g, '~ ') + '~'; const encodedQuery = encodeURIComponent(userQuery + ' || ' + query); - let url = `${endpoint.urlBase}?`; + let url = `${this.options.endpoint.urlBase}?`; - return this.installedExtensions.then(exts => { - if (endpoint.key) { - url += `${API_VERSION}`; - 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}')`; - }); + const buildNumber = this.environmentService.settingsSearchBuildId; + if (this.options.endpoint.key) { + url += `${API_VERSION}&${QUERY_TYPE}`; + } - if (buildNumber) { - filters.push(`(packageid eq 'core' and startbuildno le '${buildNumber}' and endbuildno ge '${buildNumber}')`); - url += `&$filter=${filters.join(' or ')}`; - } - } else { - url += `query=${encodedQuery}`; + if (this.options.usePost) { + const filters = this.options.newExtensionsOnly ? + [`diminish eq 'latest'`] : + await this.getVersionFilters(buildNumber); - if (buildNumber) { - url += `&build=${buildNumber}`; - } + const filterStr = encodeURIComponent(filters.join(' or ')); + const body = JSON.stringify({ + query: encodedQuery, + filters: filterStr + }); + + return { + url, + body + }; + } else { + url += `query=${encodedQuery}`; + + if (buildNumber) { + url += `&build=${buildNumber}`; } + } + + return TPromise.wrap({ url }); + } - return url; + private getVersionFilters(buildNumber?: number): TPromise { + return this.installedExtensions.then(exts => { + // 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}')`); + } + + 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}')`; + } +} + +function getSettingKey(name: string, packageId?: string): string { + return packageId ? + packageId + '_' + name : + name; } const API_VERSION = 'api-version=2016-09-01-Preview'; +const QUERY_TYPE = 'querytype=full'; function escapeSpecialChars(query: string): string { return query.replace(/\./g, ' ') @@ -257,6 +348,21 @@ function escapeSpecialChars(query: string): string { .trim(); } +function remoteSettingToISetting(remoteSetting: IRemoteSetting): IExtensionSetting { + return { + description: remoteSetting.description.split('\n'), + descriptionRanges: null, + key: remoteSetting.key, + keyRange: null, + value: remoteSetting.defaultValue, + range: null, + valueRange: null, + overrides: [], + extensionName: remoteSetting.extensionName, + extensionPublisher: remoteSetting.extensionPublisher + }; +} + class SettingMatches { private readonly descriptionMatchingWords: Map = new Map(); @@ -265,7 +371,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}_`); } @@ -273,7 +379,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]); @@ -294,10 +400,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))); + } } } @@ -315,12 +423,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); @@ -331,7 +441,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]; diff --git a/src/vs/workbench/parts/search/browser/replaceService.ts b/src/vs/workbench/parts/search/browser/replaceService.ts index c93549e85dd49..fca4c6b1a3678 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/parts/terminal/electron-browser/terminal.contribution.ts b/src/vs/workbench/parts/terminal/electron-browser/terminal.contribution.ts index 6c596e10385d9..98e2a2357b8d8 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 4e9752cafa305..3c1b515e62bf9 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, 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 d93239ac3ca6f..b533e60a755cb 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.") } } }); diff --git a/src/vs/workbench/parts/update/electron-browser/update.ts b/src/vs/workbench/parts/update/electron-browser/update.ts index a64d44eed6cf1..380a3c068ebbe 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 } 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,21 +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'; -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 +167,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); } @@ -316,6 +294,7 @@ export class UpdateContribution implements IGlobalActivity { get name() { return ''; } get cssClass() { return 'update-activity'; } + private state: UpdateState; private badgeDisposable: IDisposable = EmptyDisposable; private disposables: IDisposable[] = []; @@ -328,13 +307,7 @@ 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); - - onUpdateAvailable(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); @@ -358,74 +331,134 @@ export class UpdateContribution implements IGlobalActivity { } private onUpdateStateChange(state: UpdateState): void { - this.badgeDisposable.dispose(); + switch (state.type) { + case StateType.Idle: + if (this.state.type === StateType.CheckingForUpdates && this.state.explicit) { + this.onUpdateNotAvailable(); + } + break; + + case StateType.AvailableForDownload: + this.onUpdateAvailable(state.update); + break; - const isUpdateAvailable = isLinux - ? state === UpdateState.UpdateAvailable - : state === UpdateState.UpdateDownloaded; + case StateType.Downloaded: + this.onUpdateDownloaded(state.update); + break; - if (isUpdateAvailable) { - const badge = new NumberBadge(1, () => nls.localize('updateIsReady', "New {0} update available.", product.nameShort)); - this.badgeDisposable = this.activityService.showActivity(this.id, badge); + case StateType.Updating: + this.onUpdateUpdating(state.update); + break; + + case StateType.Ready: + this.onUpdateReady(state.update); + break; } - } - private onUpdateAvailable(version: string): void { - const currentVersion = product.commit; - const currentMillis = new Date().getTime(); - const lastKnownVersion = this.storageService.get('update/lastKnownVersion', StorageScope.GLOBAL); + let badge: IBadge | undefined = undefined; + let clazz: string | undefined; - // if version != stored version, save version and date - if (currentVersion !== lastKnownVersion) { - this.storageService.store('update/lastKnownVersion', currentVersion, StorageScope.GLOBAL); - this.storageService.store('update/updateNotificationTime', currentMillis, StorageScope.GLOBAL); + 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'; } - const updateNotificationMillis = this.storageService.getInteger('update/updateNotificationTime', StorageScope.GLOBAL, currentMillis); - const diffDays = (currentMillis - updateNotificationMillis) / (1000 * 60 * 60 * 24); + this.badgeDisposable.dispose(); - // if 5 days have passed from stored date, show message service - if (diffDays > 5) { - this.showUpdateNotification(version); + if (badge) { + this.badgeDisposable = this.activityService.showActivity(this.id, badge, clazz); } + + this.state = state; + } + + private onUpdateNotAvailable(): void { + this.messageService.show(severity.Info, nls.localize('noUpdatesAvailable', "There are no updates currently available.")); } - private showUpdateNotification(version: string): void { - const releaseNotesAction = this.instantiationService.createInstance(ShowReleaseNotesAction, version); + // linux + private onUpdateAvailable(update: IUpdate): void { + if (!this.shouldShowNotification()) { + return; + } - if (isLinux) { - const downloadAction = this.instantiationService.createInstance(DownloadAction); + const releaseNotesAction = this.instantiationService.createInstance(ShowReleaseNotesAction, update.productVersion); + 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."), - actions: [downloadAction, NotNowAction, releaseNotesAction] - }); - } else { - const applyUpdateAction = this.instantiationService.createInstance(ApplyUpdateAction); + this.messageService.show(severity.Info, { + message: nls.localize('thereIsUpdateAvailable', "There is an available update."), + actions: [downloadAction, NotNowAction, releaseNotesAction] + }); + } - this.messageService.show(severity.Info, { - message: nls.localize('updateAvailable', "{0} will be updated after it restarts.", product.nameLong), - actions: [applyUpdateAction, 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] + }); } - private onUpdateNotAvailable(explicit: boolean): void { - if (!explicit) { + // windows and mac + private onUpdateReady(update: IUpdate): void { + if (!this.shouldShowNotification()) { return; } - this.messageService.show(severity.Info, nls.localize('noUpdatesAvailable', "There are no updates currently available.")); + 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 onError(err: any): void { - this.messageService.show(severity.Error, err); + private shouldShowNotification(): boolean { + const currentVersion = product.commit; + const currentMillis = new Date().getTime(); + const lastKnownVersion = this.storageService.get('update/lastKnownVersion', StorageScope.GLOBAL); + + // if version != stored version, save version and date + if (currentVersion !== lastKnownVersion) { + this.storageService.store('update/lastKnownVersion', currentVersion, StorageScope.GLOBAL); + this.storageService.store('update/updateNotificationTime', currentMillis, StorageScope.GLOBAL); + } + + const updateNotificationMillis = this.storageService.getInteger('update/updateNotificationTime', StorageScope.GLOBAL, currentMillis); + const diffDays = (currentMillis - updateNotificationMillis) / (1000 * 60 * 60 * 24); + + return diffDays > 5; } 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), @@ -434,39 +467,49 @@ 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; + + 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 UpdateState.CheckingForUpdate: + 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.AvailableForDownload: + 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); - const updateAvailableLabel = isWindows - ? nls.localize('DownloadingUpdate', "Downloading Update...") - : nls.localize('InstallingUpdate', "Installing Update..."); + case StateType.Downloaded: + return new Action('update.install', nls.localize('installUpdate...', "Install Update..."), undefined, true, () => + this.updateService.applyUpdate()); - return new Action('update.available', updateAvailableLabel, undefined, false); + case StateType.Updating: + return new Action('update.updating', nls.localize('installingUpdate', "Installing Update..."), undefined, false); - case UpdateState.UpdateDownloaded: + 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)); } } diff --git a/src/vs/workbench/services/backup/common/backup.ts b/src/vs/workbench/services/backup/common/backup.ts index 7a46ffa491e76..0b4d35f5d0cdb 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 ff3543726ce70..18c4e2a7b5073 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, 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); @@ -149,7 +171,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 +186,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 BackupSnapshot(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 84ae8794ec775..ff58403e3819d 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(10 * 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(10 * 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/editor/test/browser/editorService.test.ts b/src/vs/workbench/services/editor/test/browser/editorService.test.ts index 196825b2f11ac..e5fd893ab7363 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/files/electron-browser/fileService.ts b/src/vs/workbench/services/files/electron-browser/fileService.ts index 5eeae64bf3a08..e97cc0ffd1d12 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 2bf1ba014e189..1fb80d9904248 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 b1475b44eb9b9..f443fec34a4e0 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 } 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'; @@ -41,6 +40,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,7 +505,7 @@ 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); } @@ -513,7 +513,7 @@ export class FileService implements IFileService { 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 +579,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,7 +608,32 @@ export class FileService implements IFileService { }); } - private doUpdateContentElevated(resource: uri, value: string, options: IUpdateContentOptions = Object.create(null)): TPromise { + 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 | ITextSnapshot, options: IUpdateContentOptions = Object.create(null)): TPromise { const absolutePath = this.toAbsolutePath(resource); // 1.) check file 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 3a97a2a5cecac..378b1b91a08d1 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')); @@ -846,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/textFileEditorModel.ts b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts index fc18f6c565646..88197be4a82f5 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); @@ -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/services/textfile/common/textFileService.ts b/src/vs/workbench/services/textfile/common/textFileService.ts index 6e5bf84bf4a63..42f1d46637001 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 = []; @@ -241,7 +244,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 +253,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); @@ -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/common/textfiles.ts b/src/vs/workbench/services/textfile/common/textfiles.ts index 4be8ffb329e92..eacc46d19705f 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'; @@ -200,7 +200,7 @@ export interface ITextFileEditorModel extends ITextEditorModel, IEncodingSupport revert(soft?: boolean): TPromise; - getValue(): string; + createSnapshot(): ITextSnapshot; isDirty(): boolean; diff --git a/src/vs/workbench/services/textfile/electron-browser/textFileService.ts b/src/vs/workbench/services/textfile/electron-browser/textFileService.ts index ecf4229c83b9a..1dcfc4c6334c0 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/services/textfile/test/textFileEditorModel.test.ts b/src/vs/workbench/services/textfile/test/textFileEditorModel.test.ts index 5e36065442d70..d747bb23b3f35 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 f5bde377342c7..3c904f154a425 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 85c63eb3e1bbb..bed57cb0b054a 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 b7b8a04dd4a0a..8edf3bb24aae8 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/extHostLanguageFeatures.test.ts b/src/vs/workbench/test/electron-browser/api/extHostLanguageFeatures.test.ts index 256ebb6879f06..e86703048ecc1 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, { 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 d6f3681d33193..d83503b679ec9 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(); }); }); diff --git a/src/vs/workbench/test/workbenchTestServices.ts b/src/vs/workbench/test/workbenchTestServices.ts index e45bc1a4b106b..e5db572cc9115 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'; @@ -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 { @@ -755,7 +756,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, @@ -856,7 +857,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); } diff --git a/src/vs/workbench/workbench.main.ts b/src/vs/workbench/workbench.main.ts index cc5fa97e31a8b..2966cea4559a8 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'; diff --git a/yarn.lock b/yarn.lock index 8c4b272370290..91fc86177733a 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-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"