diff --git a/build/gulpfile.hygiene.js b/build/gulpfile.hygiene.js index a7290782055..bd5290adc25 100644 --- a/build/gulpfile.hygiene.js +++ b/build/gulpfile.hygiene.js @@ -99,6 +99,7 @@ const copyrightFilter = [ const eslintFilter = [ 'src/**/*.js', + 'build/gulpfile.*.js', '!src/vs/loader.js', '!src/vs/css.js', '!src/vs/nls.js', diff --git a/build/gulpfile.vscode.js b/build/gulpfile.vscode.js index feff102ea13..9f746ef72cb 100644 --- a/build/gulpfile.vscode.js +++ b/build/gulpfile.vscode.js @@ -31,8 +31,6 @@ const shrinkwrap = require('../npm-shrinkwrap.json'); const crypto = require('crypto'); const i18n = require('./lib/i18n'); const glob = require('glob'); -const os = require('os'); -const cp = require('child_process'); const productDependencies = Object.keys(product.dependencies || {}); const dependencies = Object.keys(shrinkwrap.dependencies) @@ -45,8 +43,8 @@ const nodeModules = ['electron', 'original-fs'] // Build const builtInExtensions = [ - { name: 'ms-vscode.node-debug', version: '1.15.2' }, - { name: 'ms-vscode.node-debug2', version: '1.14.4' } + { name: 'ms-vscode.node-debug', version: '1.15.4' }, + { name: 'ms-vscode.node-debug2', version: '1.14.5' } ]; const excludedExtensions = [ @@ -137,7 +135,7 @@ const config = { name: product.nameLong + ' document', role: 'Editor', ostypes: ["TEXT", "utxt", "TUTX", "****"], - extensions: ["ascx", "asp", "aspx", "bash", "bash_login", "bash_logout", "bash_profile", "bashrc", "bat", "bowerrc", "c", "cc", "clj", "cljs", "cljx", "clojure", "cmd", "coffee", "config", "cpp", "cs", "cshtml", "csproj", "css", "csx", "ctp", "cxx", "dockerfile", "dot", "dtd", "editorconfig", "edn", "eyaml", "eyml", "fs", "fsi", "fsscript", "fsx", "gemspec", "gitattributes", "gitconfig", "gitignore", "go", "h", "handlebars", "hbs", "hh", "hpp", "htm", "html", "hxx", "ini", "jade", "jav", "java", "js", "jscsrc", "jshintrc", "jshtm", "json", "jsp", "less", "lua", "m", "makefile", "markdown", "md", "mdoc", "mdown", "mdtext", "mdtxt", "mdwn", "mkd", "mkdn", "ml", "mli", "php", "phtml", "pl", "pl6", "pm", "pm6", "pod", "pp", "profile", "properties", "ps1", "psd1", "psgi", "psm1", "py", "r", "rb", "rhistory", "rprofile", "rs", "rt", "scss", "sh", "shtml", "sql", "svg", "svgz", "t", "ts", "txt", "vb", "wxi", "wxl", "wxs", "xaml", "xcodeproj", "xcworkspace", "xml", "yaml", "yml", "zlogin", "zlogout", "zprofile", "zsh", "zshenv", "zshrc"], + extensions: ["ascx", "asp", "aspx", "bash", "bash_login", "bash_logout", "bash_profile", "bashrc", "bat", "bowerrc", "c", "cc", "clj", "cljs", "cljx", "clojure", "cmd", "code-workspace", "coffee", "config", "cpp", "cs", "cshtml", "csproj", "css", "csx", "ctp", "cxx", "dockerfile", "dot", "dtd", "editorconfig", "edn", "eyaml", "eyml", "fs", "fsi", "fsscript", "fsx", "gemspec", "gitattributes", "gitconfig", "gitignore", "go", "h", "handlebars", "hbs", "hh", "hpp", "htm", "html", "hxx", "ini", "jade", "jav", "java", "js", "jscsrc", "jshintrc", "jshtm", "json", "jsp", "less", "lua", "m", "makefile", "markdown", "md", "mdoc", "mdown", "mdtext", "mdtxt", "mdwn", "mkd", "mkdn", "ml", "mli", "php", "phtml", "pl", "pl6", "pm", "pm6", "pod", "pp", "profile", "properties", "ps1", "psd1", "psgi", "psm1", "py", "r", "rb", "rhistory", "rprofile", "rs", "rt", "scss", "sh", "shtml", "sql", "svg", "svgz", "t", "ts", "txt", "vb", "wxi", "wxl", "wxs", "xaml", "xcodeproj", "xcworkspace", "xml", "yaml", "yml", "zlogin", "zlogout", "zprofile", "zsh", "zshenv", "zshrc"], iconFile: 'resources/darwin/code_file.icns' }], darwinBundleURLTypes: [{ @@ -227,8 +225,7 @@ function packageTask(platform, arch, opts) { ]); const src = gulp.src(out + '/**', { base: '.' }) - .pipe(rename(function (path) { path.dirname = path.dirname.replace(new RegExp('^' + out), 'out'); })) - .pipe(util.setExecutableBit(['**/*.sh'])); + .pipe(rename(function (path) { path.dirname = path.dirname.replace(new RegExp('^' + out), 'out'); })); const root = path.resolve(path.join(__dirname, '..')); const localExtensionDescriptions = glob.sync('extensions/*/package.json') @@ -259,12 +256,9 @@ function packageTask(platform, arch, opts) { .pipe(rename(p => p.dirname = `extensions/${extension.name}/${p.dirname}`)); })); - const sources = es.merge( - src, - localExtensions, - localExtensionDependencies, - marketplaceExtensions - ).pipe(filter(['**', '!**/*.js.map'])); + const sources = es.merge(src, localExtensions, localExtensionDependencies, marketplaceExtensions) + .pipe(util.setExecutableBit(['**/*.sh'])) + .pipe(filter(['**', '!**/*.js.map'])); let version = packageJson.version; const quality = product.quality; @@ -283,6 +277,8 @@ function packageTask(platform, arch, opts) { const license = gulp.src(['LICENSES.chromium.html', 'LICENSE.txt', 'ThirdPartyNotices.txt', 'licenses/**'], { base: '.' }); + const watermark = gulp.src(['resources/letterpress.svg', 'resources/letterpress-dark.svg', 'resources/letterpress-hc.svg'], { base: '.' }); + // TODO the API should be copied to `out` during compile, not here const api = gulp.src('src/vs/vscode.d.ts').pipe(rename('out/vs/vscode.d.ts')); @@ -306,6 +302,7 @@ function packageTask(platform, arch, opts) { packageJsonStream, productJsonStream, license, + watermark, api, sources, deps @@ -371,62 +368,6 @@ gulp.task('vscode-linux-ia32-min', ['minify-vscode', 'clean-vscode-linux-ia32'], gulp.task('vscode-linux-x64-min', ['minify-vscode', 'clean-vscode-linux-x64'], packageTask('linux', 'x64', { minified: true })); gulp.task('vscode-linux-arm-min', ['minify-vscode', 'clean-vscode-linux-arm'], packageTask('linux', 'arm', { minified: true })); -// --- v8 snapshots --- - -function snapshotTask(platform, arch) { - - const destination = path.join(path.dirname(root), 'VSCode') + (platform ? '-' + platform : '') + (arch ? '-' + arch : ''); - - let command = path.join(process.cwd(), 'node_modules/.bin/mksnapshot'); - let loaderInputFilepath; - let startupBlobFilepath; - - if (platform === 'darwin') { - loaderInputFilepath = path.join(destination, 'Code - OSS.app/Contents/Resources/app/out/vs/loader.js'); - startupBlobFilepath = path.join(destination, 'Code - OSS.app/Contents/Frameworks/Electron Framework.framework/Resources/snapshot_blob.bin') - - } else if (platform === 'win32') { - command = `${command}.cmd`; - loaderInputFilepath = path.join(destination, 'resources/app/out/vs/loader.js'); - startupBlobFilepath = path.join(destination, 'snapshot_blob.bin') - - } else if (platform === 'linux') { - loaderInputFilepath = path.join(destination, 'resources/app/out/vs/loader.js'); - startupBlobFilepath = path.join(destination, 'snapshot_blob.bin') - } - - return () => { - const inputFile = fs.readFileSync(loaderInputFilepath); - const wrappedInputFile = ` - var Monaco_Loader_Init; - (function() { - var doNotInitLoader = true; - ${inputFile.toString()}; - Monaco_Loader_Init = function() { - AMDLoader.init(); - CSSLoaderPlugin.init(); - NLSLoaderPlugin.init(); - - return define; - } - })(); - `; - const wrappedInputFilepath = path.join(os.tmpdir(), 'wrapped-loader.js'); - console.log(wrappedInputFilepath); - fs.writeFileSync(wrappedInputFilepath, wrappedInputFile); - - cp.execFileSync(command, [wrappedInputFilepath, `--startup_blob`, startupBlobFilepath]); - } -} - -gulp.task('vscode-win32-ia32-snapshots', ['vscode-win32-ia32-min'], snapshotTask('win32', 'ia32')); -gulp.task('vscode-win32-x64-snapshots', ['vscode-win32-x64-min'], snapshotTask('win32', 'x64')); -gulp.task('vscode-darwin-snapshots', ['vscode-darwin-min'], snapshotTask('darwin', undefined)); -gulp.task('vscode-linux-ia32-snapshots', ['vscode-linux-ia32-min'], snapshotTask('linux', 'ia32')); -gulp.task('vscode-linux-x64-snapshots', ['vscode-linux-x64-min'], snapshotTask('linux', 'x64')); -gulp.task('vscode-linux-arm-snapshots', ['vscode-linux-arm-min'], snapshotTask('linux', 'arm')); - - // Transifex Localizations const vscodeLanguages = [ 'zh-hans', diff --git a/build/lib/extensions.js b/build/lib/extensions.js index 7989297a947..dcc8a478d09 100644 --- a/build/lib/extensions.js +++ b/build/lib/extensions.js @@ -26,6 +26,7 @@ function fromLocal(extensionPath) { .map(function (fileName) { return path.join(extensionPath, fileName); }) .map(function (filePath) { return new File({ path: filePath, + stat: fs.statSync(filePath), base: extensionPath, contents: fs.createReadStream(filePath) }); }); diff --git a/build/lib/extensions.ts b/build/lib/extensions.ts index 17349f7ee94..9bbec1b6d83 100644 --- a/build/lib/extensions.ts +++ b/build/lib/extensions.ts @@ -28,6 +28,7 @@ export function fromLocal(extensionPath: string): Stream { .map(fileName => path.join(extensionPath, fileName)) .map(filePath => new File({ path: filePath, + stat: fs.statSync(filePath), base: extensionPath, contents: fs.createReadStream(filePath) as any })); diff --git a/build/lib/snapshotLoader.js b/build/lib/snapshotLoader.js new file mode 100644 index 00000000000..eb22844b08a --- /dev/null +++ b/build/lib/snapshotLoader.js @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; +var snaps; +(function (snaps) { + var fs = require('fs'); + var path = require('path'); + var os = require('os'); + var cp = require('child_process'); + var mksnapshot = path.join(__dirname, "../../node_modules/.bin/" + (process.platform === 'win32' ? 'mksnapshot.cmd' : 'mksnapshot')); + var product = require('../../product.json'); + var arch = (process.argv.join('').match(/--arch=(.*)/) || [])[1]; + // + var loaderFilepath; + var startupBlobFilepath; + switch (process.platform) { + case 'darwin': + loaderFilepath = "VSCode-darwin/" + product.nameLong + ".app/Contents/Resources/app/out/vs/loader.js"; + startupBlobFilepath = "VSCode-darwin/" + product.nameLong + ".app/Contents/Frameworks/Electron Framework.framework/Resources/snapshot_blob.bin"; + break; + case 'win32': + case 'linux': + loaderFilepath = "VSCode-" + process.platform + "-" + arch + "/resources/app/out/vs/loader.js"; + startupBlobFilepath = "VSCode-" + process.platform + "-" + arch + "/snapshot_blob.bin"; + } + loaderFilepath = path.join(__dirname, '../../../', loaderFilepath); + startupBlobFilepath = path.join(__dirname, '../../../', startupBlobFilepath); + snapshotLoader(loaderFilepath, startupBlobFilepath); + function snapshotLoader(loaderFilepath, startupBlobFilepath) { + var inputFile = fs.readFileSync(loaderFilepath); + var wrappedInputFile = "\n\t\tvar Monaco_Loader_Init;\n\t\t(function() {\n\t\t\tvar doNotInitLoader = true;\n\t\t\t" + inputFile.toString() + ";\n\t\t\tMonaco_Loader_Init = function() {\n\t\t\t\tAMDLoader.init();\n\t\t\t\tCSSLoaderPlugin.init();\n\t\t\t\tNLSLoaderPlugin.init();\n\n\t\t\t\treturn { define, require };\n\t\t\t}\n\t\t})();\n\t\t"; + var wrappedInputFilepath = path.join(os.tmpdir(), 'wrapped-loader.js'); + console.log(wrappedInputFilepath); + fs.writeFileSync(wrappedInputFilepath, wrappedInputFile); + cp.execFileSync(mksnapshot, [wrappedInputFilepath, "--startup_blob", startupBlobFilepath]); + } +})(snaps || (snaps = {})); diff --git a/build/lib/snapshotLoader.ts b/build/lib/snapshotLoader.ts new file mode 100644 index 00000000000..4d4eef1ea94 --- /dev/null +++ b/build/lib/snapshotLoader.ts @@ -0,0 +1,63 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +namespace snaps { + + const fs = require('fs'); + const path = require('path'); + const os = require('os'); + const cp = require('child_process'); + + const mksnapshot = path.join(__dirname, `../../node_modules/.bin/${process.platform === 'win32' ? 'mksnapshot.cmd' : 'mksnapshot'}`); + const product = require('../../product.json'); + const arch = (process.argv.join('').match(/--arch=(.*)/) || [])[1]; + + // + let loaderFilepath: string; + let startupBlobFilepath: string; + + switch (process.platform) { + case 'darwin': + loaderFilepath = `VSCode-darwin/${product.nameLong}.app/Contents/Resources/app/out/vs/loader.js`; + startupBlobFilepath = `VSCode-darwin/${product.nameLong}.app/Contents/Frameworks/Electron Framework.framework/Resources/snapshot_blob.bin`; + break; + + case 'win32': + case 'linux': + loaderFilepath = `VSCode-${process.platform}-${arch}/resources/app/out/vs/loader.js`; + startupBlobFilepath = `VSCode-${process.platform}-${arch}/snapshot_blob.bin`; + } + + loaderFilepath = path.join(__dirname, '../../../', loaderFilepath); + startupBlobFilepath = path.join(__dirname, '../../../', startupBlobFilepath); + + snapshotLoader(loaderFilepath, startupBlobFilepath); + + function snapshotLoader(loaderFilepath: string, startupBlobFilepath: string): void { + + const inputFile = fs.readFileSync(loaderFilepath); + const wrappedInputFile = ` + var Monaco_Loader_Init; + (function() { + var doNotInitLoader = true; + ${inputFile.toString()}; + Monaco_Loader_Init = function() { + AMDLoader.init(); + CSSLoaderPlugin.init(); + NLSLoaderPlugin.init(); + + return { define, require }; + } + })(); + `; + const wrappedInputFilepath = path.join(os.tmpdir(), 'wrapped-loader.js'); + console.log(wrappedInputFilepath); + fs.writeFileSync(wrappedInputFilepath, wrappedInputFile); + + cp.execFileSync(mksnapshot, [wrappedInputFilepath, `--startup_blob`, startupBlobFilepath]); + } +} diff --git a/build/monaco/package.json b/build/monaco/package.json index 6e51699f41d..c31756516c7 100644 --- a/build/monaco/package.json +++ b/build/monaco/package.json @@ -48,7 +48,7 @@ "sinon": "^1.17.2", "source-map": "^0.4.4", "tslint": "^4.3.1", - "typescript": "2.3.2", + "typescript": "2.4.1", "typescript-formatter": "4.0.1", "underscore": "^1.8.2", "vinyl": "^0.4.5", diff --git a/build/package.json b/build/package.json index b3205d92bc4..cb36cf063de 100644 --- a/build/package.json +++ b/build/package.json @@ -13,7 +13,7 @@ "documentdb": "^1.11.0", "mime": "^1.3.4", "minimist": "^1.2.0", - "typescript": "2.3.4", + "typescript": "2.4.1", "xml2js": "^0.4.17" }, "scripts": { diff --git a/build/tfs/common/publish.ts b/build/tfs/common/publish.ts index e8fa56b7fa3..d959159dc86 100644 --- a/build/tfs/common/publish.ts +++ b/build/tfs/common/publish.ts @@ -131,9 +131,11 @@ async function doesAssetExist(blobService: azure.BlobService, quality: string, b } async function uploadBlob(blobService: azure.BlobService, quality: string, blobName: string, file: string): Promise { - const blobOptions = { - contentType: mime.lookup(file), - cacheControl: 'max-age=31536000, public' + const blobOptions: azure.BlobService.CreateBlockBlobRequestOptions = { + contentSettings: { + contentType: mime.lookup(file), + cacheControl: 'max-age=31536000, public' + } }; await new Promise((c, e) => blobService.createBlockBlobFromLocalFile(quality, blobName, file, blobOptions, err => err ? e(err) : c())); @@ -178,6 +180,9 @@ async function publish(commit: string, quality: string, platform: string, type: const mooncakeBlobService = azure.createBlobService(storageAccount, process.env['MOONCAKE_STORAGE_ACCESS_KEY'], `${storageAccount}.blob.core.chinacloudapi.cn`) .withFilter(new azure.ExponentialRetryPolicyFilter(20)); + // mooncake is fussy and far away, this is needed! + mooncakeBlobService.defaultClientRequestTimeoutInMs = 10 * 60 * 1000; + await Promise.all([ assertContainer(blobService, quality), assertContainer(mooncakeBlobService, quality) diff --git a/build/tfs/darwin/build.sh b/build/tfs/darwin/build.sh index afa70c29960..e342401b34e 100755 --- a/build/tfs/darwin/build.sh +++ b/build/tfs/darwin/build.sh @@ -25,6 +25,9 @@ step "Install distro dependencies" \ step "Build minified & upload source maps" \ npm run gulp -- --max_old_space_size=4096 vscode-darwin-min upload-vscode-sourcemaps +step "Create loader snapshot" + node build/lib/snapshotLoader.js + step "Run unit tests" \ ./scripts/test.sh --build --reporter dot @@ -32,4 +35,4 @@ step "Run integration tests" \ ./scripts/test-integration.sh step "Publish release" \ - ./build/tfs/darwin/release.sh \ No newline at end of file + ./build/tfs/darwin/release.sh diff --git a/build/tfs/linux/build.sh b/build/tfs/linux/build.sh index babc6fdf182..64101890aad 100755 --- a/build/tfs/linux/build.sh +++ b/build/tfs/linux/build.sh @@ -30,8 +30,11 @@ step "Install distro dependencies" \ step "Build minified" \ npm run gulp -- --max_old_space_size=4096 "vscode-linux-$ARCH-min" +step "Create loader snapshot" + node build/lib/snapshotLoader.js --arch=$ARCH + step "Run unit tests" \ ./scripts/test.sh --build --reporter dot step "Publish release" \ - ./build/tfs/linux/release.sh \ No newline at end of file + ./build/tfs/linux/release.sh diff --git a/build/tfs/win32/1_build.ps1 b/build/tfs/win32/1_build.ps1 index 8e4742da6f1..ea31fd31b04 100644 --- a/build/tfs/win32/1_build.ps1 +++ b/build/tfs/win32/1_build.ps1 @@ -36,6 +36,10 @@ step "Build minified" { exec { & npm run gulp -- --max_old_space_size=4096 "vscode-win32-$global:arch-min" } } +step "Create loader snapshot" { + exec { & node build\lib\snapshotLoader.js --arch=$global:arch } +} + step "Run unit tests" { exec { & .\scripts\test.bat --build --reporter dot } } @@ -44,4 +48,4 @@ step "Run unit tests" { # exec { & .\scripts\test-integration.bat } # } -done \ No newline at end of file +done diff --git a/build/win32/code.iss b/build/win32/code.iss index 2051718f9cf..e5c3d9d2a46 100644 --- a/build/win32/code.iss +++ b/build/win32/code.iss @@ -164,6 +164,12 @@ Root: HKCR; Subkey: "{#RegValueName}.clojure"; ValueType: string; ValueName: ""; Root: HKCR; Subkey: "{#RegValueName}.clojure\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles Root: HKCR; Subkey: "{#RegValueName}.clojure\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles +Root: HKCR; Subkey: ".code-workspace\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCR; Subkey: ".code-workspace\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.code-workspace"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCR; Subkey: "{#RegValueName}.code-workspace"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Code Workspace}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCR; Subkey: "{#RegValueName}.code-workspace\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: HKCR; Subkey: "{#RegValueName}.code-workspace\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + Root: HKCR; Subkey: ".coffee\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles Root: HKCR; Subkey: ".coffee\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.coffee"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: HKCR; Subkey: "{#RegValueName}.coffee"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,CoffeeScript}"; Flags: uninsdeletekey; Tasks: associatewithfiles diff --git a/extensions/html/package.json b/extensions/html/package.json index df231a22a2f..f79ac361538 100644 --- a/extensions/html/package.json +++ b/extensions/html/package.json @@ -24,7 +24,6 @@ { "id": "html", "extensions": [ - ".rhtml", ".html", ".htm", ".shtml", @@ -35,7 +34,8 @@ ".aspx", ".jshtm", ".volt", - ".ejs" + ".ejs", + ".rhtml" ], "aliases": [ "HTML", diff --git a/extensions/javascript/syntaxes/JavaScript.tmLanguage.json b/extensions/javascript/syntaxes/JavaScript.tmLanguage.json index 7610157bb85..6a9a32ac03c 100644 --- a/extensions/javascript/syntaxes/JavaScript.tmLanguage.json +++ b/extensions/javascript/syntaxes/JavaScript.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/Microsoft/TypeScript-TmLanguage/commit/9be58bc51f179fd4119dbd5caaa7693a381a13b5", + "version": "https://github.com/Microsoft/TypeScript-TmLanguage/commit/648a036db2bad78ee93463269ec49ed91ee5aa91", "name": "JavaScript (with React support)", "scopeName": "source.js", "fileTypes": [ @@ -103,7 +103,7 @@ "patterns": [ { "name": "meta.var-single-variable.expr.js", - "begin": "(?x)([_$[:alpha:]][_$[:alnum:]]*)(?=\\s*\n (=\\s*(\n ((async\\s+)?(\n (function\\s*[(<]) |\n (function\\s+) |\n ([_$[:alpha:]][_$[:alnum:]]*\\s*=>)\n )) |\n ((async\\s*)?(\n ([(]\\s*(([)]\\s*:)|([_$[:alpha:]][_$[:alnum:]]*\\s*:)|(\\.\\.\\.) )) |\n ([<]\\s*[_$[:alpha:]][_$[:alnum:]]*((\\s+extends\\s*[^=>])|(\\s*[,]))) |\n ((<([^<>=]|=[^<]|\\<([^=<>]|=[^<])+\\>)+>\\s*)?\\(([^()]|\\([^()]*\\))*\\)(\\s*:\\s*(.)*)?\\s*=>)\n ))\n )) |\n (:\\s*(\n (<) |\n ([(]\\s*(\n ([)]) |\n (\\.\\.\\.) |\n ([_$[:alnum:]]+\\s*(\n ([:,?=])|\n ([)]\\s*=>)\n ))\n ))\n ))\n)", + "begin": "(?x)([_$[:alpha:]][_$[:alnum:]]*)(?=\\s*\n (=\\s*(\n ((async\\s+)?(\n (function\\s*[(<*]) |\n (function\\s+) |\n ([_$[:alpha:]][_$[:alnum:]]*\\s*=>)\n )) |\n ((async\\s*)?(\n ([(]\\s*(([)]\\s*:)|([_$[:alpha:]][_$[:alnum:]]*\\s*:)|(\\.\\.\\.) )) |\n ([<]\\s*[_$[:alpha:]][_$[:alnum:]]*((\\s+extends\\s*[^=>])|(\\s*[,]))) |\n ((<([^<>=]|=[^<]|\\<([^=<>]|=[^<])+\\>)+>\\s*)?\\(([^()]|\\([^()]*\\))*\\)(\\s*:\\s*(.)*)?\\s*=>)\n ))\n )) |\n (:\\s*(\n (<) |\n ([(]\\s*(\n ([)]) |\n (\\.\\.\\.) |\n ([_$[:alnum:]]+\\s*(\n ([:,?=])|\n ([)]\\s*=>)\n ))\n ))\n ))\n)", "beginCaptures": { "1": { "name": "meta.definition.variable.js entity.name.function.js" @@ -203,7 +203,7 @@ "include": "#comment" }, { - "begin": "(?=(([_$[:alpha:]][_$[:alnum:]]*)|(\\'.*\\')|(\\\".*\\\")|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])+\\]))\\s*(:))", + "begin": "(?=(([_$[:alpha:]][_$[:alnum:]]*)|(\\'([^\\'\\\\]|\\\\\\'|\\\\)*\\')|(\\\"([^\\\"\\\\]|\\\\\\\"|\\\\)*\\\")|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])+\\]))\\s*(:))", "end": "(?=,|\\})", "patterns": [ { @@ -229,7 +229,7 @@ ] }, "object-binding-element-propertyName": { - "begin": "(?=(([_$[:alpha:]][_$[:alnum:]]*)|(\\'.*\\')|(\\\".*\\\")|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])+\\]))\\s*(:))", + "begin": "(?=(([_$[:alpha:]][_$[:alnum:]]*)|(\\'([^\\'\\\\]|\\\\\\'|\\\\)*\\')|(\\\"([^\\\"\\\\]|\\\\\\\"|\\\\)*\\\")|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])+\\]))\\s*(:))", "end": "(:)", "endCaptures": { "0": { @@ -366,7 +366,7 @@ "include": "#function-expression" }, { - "include": "#class-or-interface-declaration" + "include": "#class-expression" }, { "include": "#arrow-function" @@ -457,7 +457,10 @@ "include": "#function-declaration" }, { - "include": "#class-or-interface-declaration" + "include": "#class-declaration" + }, + { + "include": "#interface-declaration" }, { "include": "#type-declaration" @@ -508,7 +511,7 @@ "name": "entity.name.type.alias.js" } }, - "end": "(?=[};]|\\bvar\\b|\\blet\\b|\\bconst\\b|\\btype\\b|\\bfunction\\b|\\bclass\\b|\\binterface\\b|\\bnamespace\\b|\\bmodule\\b|\\bimport\\b|\\benum\\b|\\bdeclare\\b|\\bexport\\b|\\babstract\\b|\\basync\\b)", + "end": "(?=[};]|\\babstract\\b|\\basync\\b|\\bclass\\b|\\bconst\\b|\\bdeclare\\b|\\benum\\b|\\bexport\\b|\\bfunction\\b|\\bimport\\b|\\binterface\\b|\\blet\\b|\\bmodule\\b|\\bnamespace\\b|\\btype\\b|\\bvar\\b)", "patterns": [ { "include": "#comment" @@ -586,7 +589,7 @@ ] }, { - "begin": "(?=((\\'.*\\')|(\\\".*\\\")|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])+\\])))", + "begin": "(?=((\\'([^\\'\\\\]|\\\\\\'|\\\\)*\\')|(\\\"([^\\\"\\\\]|\\\\\\\"|\\\\)*\\\")|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])+\\])))", "end": "(?=,|\\}|$)", "patterns": [ { @@ -621,7 +624,7 @@ "name": "storage.type.namespace.js" } }, - "end": "(?<=\\})", + "end": "(?<=\\})|(?=;|\\babstract\\b|\\basync\\b|\\bclass\\b|\\bconst\\b|\\bdeclare\\b|\\benum\\b|\\bexport\\b|\\bfunction\\b|\\bimport\\b|\\binterface\\b|\\blet\\b|\\bmodule\\b|\\bnamespace\\b|\\btype\\b|\\bvar\\b)", "patterns": [ { "include": "#comment" @@ -698,7 +701,7 @@ "name": "keyword.operator.assignment.js" } }, - "end": "(?=;|$)", + "end": "(?=;|$|^)", "patterns": [ { "include": "#comment" @@ -733,7 +736,7 @@ "name": "keyword.control.import.js" } }, - "end": "(?=;|$)", + "end": "(?=;|$|^)", "patterns": [ { "include": "#import-export-declaration" @@ -773,7 +776,7 @@ "name": "keyword.control.default.js" } }, - "end": "(?=;|\\bexport\\b|\\bfunction\\b|\\bclass\\b|\\binterface\\b|\\blet\\b|\\bvar\\b|\\bconst\\b|\\bimport\\b|\\benum\\b|\\bnamespace\\b|\\bmodule\\b|\\btype\\b|\\babstract\\b|\\bdeclare\\b|\\basync\\b|$)", + "end": "(?=;|$|\\babstract\\b|\\basync\\b|\\bclass\\b|\\bconst\\b|\\bdeclare\\b|\\benum\\b|\\bexport\\b|\\bfunction\\b|\\bimport\\b|\\binterface\\b|\\blet\\b|\\bmodule\\b|\\bnamespace\\b|\\btype\\b|\\bvar\\b)", "patterns": [ { "include": "#expression" @@ -788,7 +791,7 @@ "name": "keyword.control.export.js" } }, - "end": "(?=;|\\bexport\\b|\\bfunction\\b|\\bclass\\b|\\binterface\\b|\\blet\\b|\\bvar\\b|\\bconst\\b|\\bimport\\b|\\benum\\b|\\bnamespace\\b|\\bmodule\\b|\\btype\\b|\\babstract\\b|\\bdeclare\\b|\\basync\\b|$)", + "end": "(?=;|$|\\babstract\\b|\\basync\\b|\\bclass\\b|\\bconst\\b|\\bdeclare\\b|\\benum\\b|\\bexport\\b|\\bfunction\\b|\\bimport\\b|\\binterface\\b|\\blet\\b|\\bmodule\\b|\\bnamespace\\b|\\btype\\b|\\bvar\\b)", "patterns": [ { "include": "#import-export-declaration" @@ -883,93 +886,101 @@ } ] }, - "class-or-interface-declaration": { + "class-declaration": { + "name": "meta.class.js", + "begin": "(?)\n )) |\n ((async\\s*)?(\n ([(]\\s*(([)]\\s*:)|([_$[:alpha:]][_$[:alnum:]]*\\s*:)|(\\.\\.\\.) )) |\n ([<]\\s*[_$[:alpha:]][_$[:alnum:]]*((\\s+extends\\s*[^=>])|(\\s*[,]))) |\n ((<([^<>=]|=[^<]|\\<([^=<>]|=[^<])+\\>)+>\\s*)?\\(([^()]|\\([^()]*\\))*\\)(\\s*:\\s*(.)*)?\\s*=>)\n ))\n )) |\n (:\\s*(\n (<) |\n ([(]\\s*(\n ([)]) |\n (\\.\\.\\.) |\n ([_$[:alnum:]]+\\s*(\n ([:,?=])|\n ([)]\\s*=>)\n ))\n ))\n ))\n)" + "match": "(?x)([_$[:alpha:]][_$[:alnum:]]*)(?=(\\?\\s*)?\\s*\n (=\\s*(\n ((async\\s+)?(\n (function\\s*[(<*]) |\n (function\\s+) |\n ([_$[:alpha:]][_$[:alnum:]]*\\s*=>)\n )) |\n ((async\\s*)?(\n ([(]\\s*(([)]\\s*:)|([_$[:alpha:]][_$[:alnum:]]*\\s*:)|(\\.\\.\\.) )) |\n ([<]\\s*[_$[:alpha:]][_$[:alnum:]]*((\\s+extends\\s*[^=>])|(\\s*[,]))) |\n ((<([^<>=]|=[^<]|\\<([^=<>]|=[^<])+\\>)+>\\s*)?\\(([^()]|\\([^()]*\\))*\\)(\\s*:\\s*(.)*)?\\s*=>)\n ))\n )) |\n (:\\s*(\n (<) |\n ([(]\\s*(\n ([)]) |\n (\\.\\.\\.) |\n ([_$[:alnum:]]+\\s*(\n ([:,?=])|\n ([)]\\s*=>)\n ))\n ))\n ))\n)" }, { "name": "meta.definition.property.js variable.object.property.js", @@ -1188,7 +1199,7 @@ "patterns": [ { "name": "meta.method.declaration.js", - "begin": "(?)\n )) |\n ((async\\s*)?(\n ([(]\\s*(([)]\\s*:)|([_$[:alpha:]][_$[:alnum:]]*\\s*:)|(\\.\\.\\.) )) |\n ([<]\\s*[_$[:alpha:]][_$[:alnum:]]*((\\s+extends\\s*[^=>])|(\\s*[,]))) |\n ((<([^<>=]|=[^<]|\\<([^=<>]|=[^<])+\\>)+>\\s*)?\\(([^()]|\\([^()]*\\))*\\)(\\s*:\\s*(.)*)?\\s*=>)\n ))\n )) |\n (:\\s*(\n (<) |\n ([(]\\s*(\n ([)]) |\n (\\.\\.\\.) |\n ([_$[:alnum:]]+\\s*(\n ([:,?=])|\n ([)]\\s*=>)\n ))\n ))\n ))\n)", + "match": "(?x)(?:\\s*\\b(public|private|protected|readonly)\\s+)?(\\.\\.\\.)?\\s*(?)\n )) |\n ((async\\s*)?(\n ([(]\\s*(([)]\\s*:)|([_$[:alpha:]][_$[:alnum:]]*\\s*:)|(\\.\\.\\.) )) |\n ([<]\\s*[_$[:alpha:]][_$[:alnum:]]*((\\s+extends\\s*[^=>])|(\\s*[,]))) |\n ((<([^<>=]|=[^<]|\\<([^=<>]|=[^<])+\\>)+>\\s*)?\\(([^()]|\\([^()]*\\))*\\)(\\s*:\\s*(.)*)?\\s*=>)\n ))\n )) |\n (:\\s*(\n (<) |\n ([(]\\s*(\n ([)]) |\n (\\.\\.\\.) |\n ([_$[:alnum:]]+\\s*(\n ([:,?=])|\n ([)]\\s*=>)\n ))\n ))\n ))\n)", "captures": { "1": { "name": "storage.modifier.js" @@ -1655,7 +1666,7 @@ "include": "#comment" }, { - "begin": "(?=(([_$[:alpha:]][_$[:alnum:]]*)|(\\'.*\\')|(\\\".*\\\")|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])+\\]))\\s*(:))", + "begin": "(?=(([_$[:alpha:]][_$[:alnum:]]*)|(\\'([^\\'\\\\]|\\\\\\'|\\\\)*\\')|(\\\"([^\\\"\\\\]|\\\\\\\"|\\\\)*\\\")|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])+\\]))\\s*(:))", "end": "(?=,|\\})", "patterns": [ { @@ -2541,7 +2552,7 @@ "include": "#object-identifiers" }, { - "match": "(?x)(?:(\\.)\\s*)?([_$[:alpha:]][_$[:alnum:]]*)(?=\\s*=\\s*(\n ((async\\s+)?(\n (function\\s*[(<]) |\n (function\\s+) |\n ([_$[:alpha:]][_$[:alnum:]]*\\s*=>)\n )) |\n ((async\\s*)?(\n ([(]\\s*(([)]\\s*:)|([_$[:alpha:]][_$[:alnum:]]*\\s*:)|(\\.\\.\\.) )) |\n ([<]\\s*[_$[:alpha:]][_$[:alnum:]]*((\\s+extends\\s*[^=>])|(\\s*[,]))) |\n ((<([^<>=]|=[^<]|\\<([^=<>]|=[^<])+\\>)+>\\s*)?\\(([^()]|\\([^()]*\\))*\\)(\\s*:\\s*(.)*)?\\s*=>)\n ))\n))", + "match": "(?x)(?:(\\.)\\s*)?([_$[:alpha:]][_$[:alnum:]]*)(?=\\s*=\\s*(\n ((async\\s+)?(\n (function\\s*[(<*]) |\n (function\\s+) |\n ([_$[:alpha:]][_$[:alnum:]]*\\s*=>)\n )) |\n ((async\\s*)?(\n ([(]\\s*(([)]\\s*:)|([_$[:alpha:]][_$[:alnum:]]*\\s*:)|(\\.\\.\\.) )) |\n ([<]\\s*[_$[:alpha:]][_$[:alnum:]]*((\\s+extends\\s*[^=>])|(\\s*[,]))) |\n ((<([^<>=]|=[^<]|\\<([^=<>]|=[^<])+\\>)+>\\s*)?\\(([^()]|\\([^()]*\\))*\\)(\\s*:\\s*(.)*)?\\s*=>)\n ))\n))", "captures": { "1": { "name": "punctuation.accessor.js" @@ -2631,13 +2642,13 @@ "name": "keyword.operator.new.js" } }, - "end": "(?<=\\))|(?=[;),}]|$|((?)\n )) |\n ((async\\s*)?(\n ([(]\\s*(([)]\\s*:)|([_$[:alpha:]][_$[:alnum:]]*\\s*:)|(\\.\\.\\.) )) |\n ([<]\\s*[_$[:alpha:]][_$[:alnum:]]*((\\s+extends\\s*[^=>])|(\\s*[,]))) |\n ((<([^<>=]|=[^<]|\\<([^=<>]|=[^<])+\\>)+>\\s*)?\\(([^()]|\\([^()]*\\))*\\)(\\s*:\\s*(.)*)?\\s*=>)\n ))\n)))", - "beginCaptures": { + "match": "(?x)(?:([_$[:alpha:]][_$[:alnum:]]*)\\s*(?=:\\s*(\n ((async\\s+)?(\n (function\\s*[(<*]) |\n (function\\s+) |\n ([_$[:alpha:]][_$[:alnum:]]*\\s*=>)\n )) |\n ((async\\s*)?(\n ([(]\\s*(([)]\\s*:)|([_$[:alpha:]][_$[:alnum:]]*\\s*:)|(\\.\\.\\.) )) |\n ([<]\\s*[_$[:alpha:]][_$[:alnum:]]*((\\s+extends\\s*[^=>])|(\\s*[,]))) |\n ((<([^<>=]|=[^<]|\\<([^=<>]|=[^<])+\\>)+>\\s*)?\\(([^()]|\\([^()]*\\))*\\)(\\s*:\\s*(.)*)?\\s*=>)\n ))\n)))", + "captures": { "0": { "name": "meta.object-literal.key.js" }, "1": { "name": "entity.name.function.js" - }, - "2": { - "name": "punctuation.separator.key-value.js" } - }, - "end": "(?=,|\\})", - "patterns": [ - { - "include": "#expression" - } - ] + } }, { "name": "meta.object.member.js", - "begin": "(?:[_$[:alpha:]][_$[:alnum:]]*)\\s*(:)", - "beginCaptures": { + "match": "(?:[_$[:alpha:]][_$[:alnum:]]*)\\s*(?=:)", + "captures": { "0": { "name": "meta.object-literal.key.js" - }, - "1": { - "name": "punctuation.separator.key-value.js" } }, - "end": "(?=,|\\})", - "patterns": [ - { - "include": "#expression" - } - ] + "end": "(?=,|\\})" }, { "name": "meta.object.member.js", @@ -2764,11 +2741,29 @@ } } }, + { + "include": "#object-member-body" + }, { "include": "#punctuation-comma" } ] }, + "object-member-body": { + "name": "meta.object.member.js", + "begin": ":", + "beginCaptures": { + "0": { + "name": "meta.object-literal.key.js punctuation.separator.key-value.js" + } + }, + "end": "(?=,|\\})", + "patterns": [ + { + "include": "#expression" + } + ] + }, "expression-operators": { "patterns": [ { @@ -3194,7 +3189,7 @@ "patterns": [ { "name": "constant.other.character-class.range.regexp", - "match": "(?:.|(\\\\(?:[0-7]{3}|x\\h\\h|u\\h\\h\\h\\h))|(\\\\c[A-Z])|(\\\\.))\\-(?:[^\\]\\\\]|(\\\\(?:[0-7]{3}|x\\h\\h|u\\h\\h\\h\\h))|(\\\\c[A-Z])|(\\\\.))", + "match": "(?:.|(\\\\(?:[0-7]{3}|x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4}))|(\\\\c[A-Z])|(\\\\.))\\-(?:[^\\]\\\\]|(\\\\(?:[0-7]{3}|x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4}))|(\\\\c[A-Z])|(\\\\.))", "captures": { "1": { "name": "constant.character.numeric.regexp" @@ -3234,7 +3229,7 @@ }, { "name": "constant.character.numeric.regexp", - "match": "\\\\([0-7]{3}|x\\h\\h|u\\h\\h\\h\\h)" + "match": "\\\\([0-7]{3}|x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4})" }, { "name": "constant.character.control.regexp", @@ -3284,7 +3279,7 @@ }, "string-character-escape": { "name": "constant.character.escape.js", - "match": "\\\\(x\\h{2}|[0-2][0-7]{0,2}|3[0-6][0-7]?|37[0-7]?|[4-7][0-7]?|.|$)" + "match": "\\\\(x[0-9A-Fa-f]{2}|[0-2][0-7]{0,2}|3[0-6][0-7]?|37[0-7]?|[4-7][0-7]?|.|$)" }, "template-substitution-element": { "name": "meta.template.expression.js", @@ -3471,10 +3466,16 @@ }, { "name": "comment.block.js", - "begin": "/\\*", + "begin": "(/\\*)(?:\\s*((@)internal)(?=\\s|(\\*/)))?", "beginCaptures": { - "0": { + "1": { "name": "punctuation.definition.comment.js" + }, + "2": { + "name": "storage.type.internaldeclaration.js" + }, + "3": { + "name": "punctuation.decorator.internaldeclaration.js" } }, "end": "\\*/", @@ -3485,13 +3486,22 @@ } }, { - "begin": "(^[ \\t]+)?(//)", + "begin": "(^[ \\t]+)?((//)(?:\\s*((@)internal)(?=\\s|$))?)", "beginCaptures": { "1": { "name": "punctuation.whitespace.comment.leading.js" }, "2": { - "name": "comment.line.double-slash.js punctuation.definition.comment.js" + "name": "comment.line.double-slash.js" + }, + "3": { + "name": "punctuation.definition.comment.js" + }, + "4": { + "name": "storage.type.internaldeclaration.js" + }, + "5": { + "name": "punctuation.decorator.internaldeclaration.js" } }, "end": "(?=^)", @@ -3501,7 +3511,7 @@ }, "directives": { "name": "comment.line.triple-slash.directive.js", - "begin": "^(///)\\s*(?=<(reference|amd-dependency|amd-module)(\\s+(path|types|no-default-lib|name)\\s*=\\s*((\\'.*\\')|(\\\".*\\\")))+\\s*/>\\s*$)", + "begin": "^(///)\\s*(?=<(reference|amd-dependency|amd-module)(\\s+(path|types|no-default-lib|name)\\s*=\\s*((\\'([^\\'\\\\]|\\\\\\'|\\\\)*\\')|(\\\"([^\\\"\\\\]|\\\\\\\"|\\\\)*\\\")))+\\s*/>\\s*$)", "beginCaptures": { "1": { "name": "punctuation.definition.comment.js" @@ -3545,7 +3555,7 @@ "docblock": { "patterns": [ { - "match": "(?x)\n((@)access)\n\\s+\n(private|protected|public)\n\\b", + "match": "(?x)\n((@)(?:access|api))\n\\s+\n(private|protected|public)\n\\b", "captures": { "1": { "name": "storage.type.class.jsdoc" @@ -3669,7 +3679,7 @@ } }, { - "match": "(?x)\n((@)see)\n\\s+\n(?:\n # URL\n (\n (?=https?://)\n (?:[^\\s*]|\\*[^/])+\n )\n |\n # JSDoc namepath\n (\n (?!https?://)\n (?:[^@\\s*/]|\\*[^/])+\n )\n)", + "match": "(?x)\n((@)see)\n\\s+\n(?:\n # URL\n (\n (?!\n # Avoid matching bare URIs (also acceptable as links)\n https?://\n |\n # Avoid matching {@inline tags}; we match those below\n (?:\\[[^\\[\\]]*\\])? # Possible description [preceding]{@tag}\n {@(?:link|linkcode|linkplain|tutorial)\\b\n )\n # Matched namepath\n (?:[^\\s*]|\\*[^/])+\n )\n |\n # JSDoc namepath\n (\n (?!https?://)\n (?:[^@\\s*/]|\\*[^/])+\n )\n)", "captures": { "1": { "name": "storage.type.class.jsdoc" @@ -3761,6 +3771,24 @@ }, { "name": "variable.other.jsdoc", + "match": "(?x)\n(\\[)\\s*\n[\\w$]+\n(?:\n (?:\\[\\])? # Foo[ ].bar properties within an array\n \\. # Foo.Bar namespaced parameter\n [\\w$]+\n)*\n(?:\n \\s*\n (=) # [foo=bar] Default parameter value\n \\s*\n (\n # The inner regexes are to stop the match early at */ and to not stop at escaped quotes\n (?:\n \"(?:(?:\\*(?!/))|(?:\\\\(?!\"))|[^*\\\\])*?\" | # [foo=\"bar\"] Double-quoted\n '(?:(?:\\*(?!/))|(?:\\\\(?!'))|[^*\\\\])*?' | # [foo='bar'] Single-quoted\n \\[ (?:(?:\\*(?!/))|[^*])*? \\] | # [foo=[1,2]] Array literal\n (?:(?:\\*(?!/))|[^*])*? # Everything else\n )*\n )\n)?\n\\s*(?:(\\])((?:[^*\\s]|\\*[^\\s/])+)?|(?=\\*/))", + "captures": { + "1": { + "name": "punctuation.definition.optional-value.begin.bracket.square.jsdoc" + }, + "2": { + "name": "keyword.operator.assignment.jsdoc" + }, + "3": { + "name": "source.embedded.js" + }, + "4": { + "name": "punctuation.definition.optional-value.end.bracket.square.jsdoc" + }, + "5": { + "name": "invalid.illegal.syntax.jsdoc" + } + }, "begin": "\\[", "end": "\\]|(?=\\*/)", "patterns": [ @@ -3859,7 +3887,7 @@ }, { "name": "storage.type.class.jsdoc", - "match": "(?x) (@) (?:abstract|access|alias|arg|argument|async|attribute|augments|author|beta|borrows|bubbles |callback|chainable|class|classdesc|code|config|const|constant|constructor|constructs|copyright |default|defaultvalue|define|deprecated|desc|description|dict|emits|enum|event|example|exception |exports?|extends|extension(?:_?for)?|external|externs|file|fileoverview|final|fires|for|func |function|global|host|ignore|implements|implicitCast|inherit[Dd]oc|inner|instance|interface|kind |lends|license|listens|main|member|memberof!?|method|mixes|mixins?|modifies|module|name|namespace |noalias|nocollapse|nocompile|nosideeffects|override|overview|package|param|preserve|private|prop |property|protected|public|read[Oo]nly|record|require[ds]|returns?|see|since|static|struct|submodule |summary|suppress|template|this|throws|todo|tutorial|type|typedef|unrestricted|uses|var|variation |version|virtual|writeOnce) \\b", + "match": "(?x) (@) (?:abstract|access|alias|api|arg|argument|async|attribute|augments|author|beta|borrows|bubbles |callback|chainable|class|classdesc|code|config|const|constant|constructor|constructs|copyright |default|defaultvalue|define|deprecated|desc|description|dict|emits|enum|event|example|exception |exports?|extends|extension(?:_?for)?|external|externs|file|fileoverview|final|fires|for|func |function|global|host|ignore|implements|implicitCast|inherit[Dd]oc|inner|instance|interface |internal|kind|lends|license|listens|main|member|memberof!?|method|mixes|mixins?|modifies|module |name|namespace|noalias|nocollapse|nocompile|nosideeffects|override|overview|package|param|preserve |private|prop|property|protected|public|read[Oo]nly|record|require[ds]|returns?|see|since|static |struct|submodule|summary|suppress|template|this|throws|todo|tutorial|type|typedef|unrestricted |uses|var|variation|version|virtual|writeOnce) \\b", "captures": { "1": { "name": "punctuation.definition.block.tag.jsdoc" @@ -4125,6 +4153,15 @@ "name": "invalid.illegal.attribute.js", "match": "\\S+" }, + "jsx-tag-without-attributes-in-expression": { + "begin": "(?x)\n (?<=[({\\[,?=>:*]|&&|\\|\\||\\?|\\Wreturn|^return|\\Wdefault|^)\\s*\n (?=(<)\\s*((?:[a-z][a-z0-9]*|([_$a-zA-Z][-$\\w.]*))(?))", + "end": "(?!\\s*(<)\\s*((?:[a-z][a-z0-9]*|([_$a-zA-Z][-$\\w.]*))(?))", + "patterns": [ + { + "include": "#jsx-tag-without-attributes" + } + ] + }, "jsx-tag-without-attributes": { "name": "meta.tag.without-attributes.js", "begin": "(<)\\s*((?:[a-z][a-z0-9]*|([_$a-zA-Z][-$\\w.]*))(?)", @@ -4165,7 +4202,7 @@ ] }, "jsx-tag-in-expression": { - "begin": "(?x)\n (?<=[({\\[,?=>:*]|&&|\\|\\||\\?|\\Wreturn|^return|\\Wdefault|^)\\s*\n (?!(<)\\s*([_$a-zA-Z][-$\\w.]*(?)) #look ahead is not start of tag without attributes\n (?!<\\s*[_$[:alpha:]][_$[:alnum:]]*((\\s+extends\\s+[^=>])|,)) # look ahead is not type parameter of arrow\n (?=(<)\\s*\n ([_$a-zA-Z][-$\\w.]*(?))", + "begin": "(?x)\n (?<=[({\\[,?=>:*]|&&|\\|\\||\\?|\\Wreturn|^return|\\Wdefault|^)\\s*\n (?!<\\s*[_$[:alpha:]][_$[:alnum:]]*((\\s+extends\\s+[^=>])|,)) # look ahead is not type parameter of arrow\n (?=(<)\\s*\n ([_$a-zA-Z][-$\\w.]*(?))", "end": "(/>)|(?:())", "endCaptures": { "0": { @@ -4296,7 +4333,7 @@ "jsx": { "patterns": [ { - "include": "#jsx-tag-without-attributes" + "include": "#jsx-tag-without-attributes-in-expression" }, { "include": "#jsx-tag-in-expression" diff --git a/extensions/json/package.json b/extensions/json/package.json index 92fd2f2645c..30a27d75afa 100644 --- a/extensions/json/package.json +++ b/extensions/json/package.json @@ -31,7 +31,8 @@ ".jscsrc", ".eslintrc", ".babelrc", - ".webmanifest" + ".webmanifest", + ".code-workspace" ], "mimetypes": [ "application/json", diff --git a/extensions/less/package.json b/extensions/less/package.json index 865edef01d1..b23dfe356b9 100644 --- a/extensions/less/package.json +++ b/extensions/less/package.json @@ -18,6 +18,21 @@ "language": "less", "scopeName": "source.css.less", "path": "./syntaxes/less.tmLanguage.json" - }] + }], + "problemMatchers": [ + { + "name": "lessc", + "label": "Lessc compiler", + "owner": "lessc", + "fileLocation": "absolute", + "pattern": { + "regexp": "(.*)\\sin\\s(.*)\\son line\\s(\\d+),\\scolumn\\s(\\d+)", + "message": 1, + "file": 2, + "line": 3, + "column": 4 + } + } + ] } } \ No newline at end of file diff --git a/extensions/npm/package.json b/extensions/npm/package.json index 0570253af1f..c3fb3e0129e 100644 --- a/extensions/npm/package.json +++ b/extensions/npm/package.json @@ -38,6 +38,11 @@ ], "default": "on", "description": "%config.npm.autoDetect%" + }, + "npm.runSilent": { + "type": "boolean", + "default": false, + "description": "%config.npm.runSilent%" } } }, diff --git a/extensions/npm/package.nls.json b/extensions/npm/package.nls.json index bd7c911cda4..78227cb0df1 100644 --- a/extensions/npm/package.nls.json +++ b/extensions/npm/package.nls.json @@ -1,3 +1,4 @@ { - "config.npm.autoDetect": "Controls whether auto detection of npm scripts is on or off. Default is on." + "config.npm.autoDetect": "Controls whether auto detection of npm scripts is on or off. Default is on.", + "config.npm.runSilent": "Run npm commands with the `--silent` option" } \ No newline at end of file diff --git a/extensions/npm/src/main.ts b/extensions/npm/src/main.ts index a52b9de7cb3..9570b8c895c 100644 --- a/extensions/npm/src/main.ts +++ b/extensions/npm/src/main.ts @@ -87,8 +87,16 @@ function isTestTask(name: string): boolean { return false; } +function getNpmCommandLine(script:string): string { + if (vscode.workspace.getConfiguration('npm').get('runSilent')) { + return `npm --silent run ${script}`; + } + return `npm run ${script}` +} + async function getNpmScriptsAsTasks(): Promise { let workspaceRoot = vscode.workspace.rootPath; + let emptyTasks: vscode.Task[] = []; if (!workspaceRoot) { @@ -100,6 +108,11 @@ async function getNpmScriptsAsTasks(): Promise { return emptyTasks; } + let silent = ''; + if (vscode.workspace.getConfiguration('npm').get('runSilent')) { + silent = '--silent'; + } + try { var contents = await readFile(packageJson); var json = JSON.parse(contents); @@ -113,7 +126,7 @@ async function getNpmScriptsAsTasks(): Promise { type: 'npm', script: each }; - const task = new vscode.Task(kind, `run ${each}`, 'npm', new vscode.ShellExecution(`npm run ${each}`)); + const task = new vscode.Task(kind, `run ${each}`, 'npm', new vscode.ShellExecution(getNpmCommandLine(each))); const lowerCaseTaskName = each.toLowerCase(); if (isBuildTask(lowerCaseTaskName)) { task.group = vscode.TaskGroup.Build; diff --git a/extensions/scss/package.json b/extensions/scss/package.json index dc291c2670f..49c2513a128 100644 --- a/extensions/scss/package.json +++ b/extensions/scss/package.json @@ -18,6 +18,44 @@ "language": "scss", "scopeName": "source.css.scss", "path": "./syntaxes/scss.json" - }] + }], + "problemMatchers": [ + { + "name": "node-sass", + "label": "Node Sass Compiler", + "owner": "node-sass", + "fileLocation": "absolute", + "pattern": [ + { + "regexp": "^{$" + }, + { + "regexp": "\\s*\"status\":\\s\\d+," + }, + { + "regexp": "\\s*\"file\":\\s\"(.*)\",", + "file": 1 + }, + { + "regexp": "\\s*\"line\":\\s(\\d+),", + "line": 1 + }, + { + "regexp": "\\s*\"column\":\\s(\\d+),", + "column": 1 + }, + { + "regexp": "\\s*\"message\":\\s\"(.*)\",", + "message": 1 + }, + { + "regexp": "\\s*\"formatted\":\\s(.*)" + }, + { + "regexp": "^}$" + } + ] + } + ] } } \ No newline at end of file diff --git a/extensions/theme-monokai-dimmed/themes/dimmed-monokai-color-theme.json b/extensions/theme-monokai-dimmed/themes/dimmed-monokai-color-theme.json index 71e9ef02407..60277bbbf06 100644 --- a/extensions/theme-monokai-dimmed/themes/dimmed-monokai-color-theme.json +++ b/extensions/theme-monokai-dimmed/themes/dimmed-monokai-color-theme.json @@ -191,7 +191,7 @@ }, { "name": "Class Variable", - "scope": "variable.js, punctuation.separator.variable", + "scope": "variable.other, variable.js, punctuation.separator.variable", "settings": { "fontStyle": "\n \t\t\t", "foreground": "#6089B4" @@ -237,6 +237,14 @@ "foreground": "#9872A2" } }, + { + "name": "Function Call Variable", + "scope": "variable.other.property", + "settings": { + "fontStyle": "\n \t\t\t", + "foreground": "#9872A2" + } + }, { "name": "Keyword Control", "scope": "keyword.control", diff --git a/extensions/typescript/src/features/bufferSyncSupport.ts b/extensions/typescript/src/features/bufferSyncSupport.ts index 0fc4ab3eb23..e76816e5b8f 100644 --- a/extensions/typescript/src/features/bufferSyncSupport.ts +++ b/extensions/typescript/src/features/bufferSyncSupport.ts @@ -3,17 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as cp from 'child_process'; import * as fs from 'fs'; -import { workspace, window, TextDocument, TextDocumentChangeEvent, TextDocumentContentChangeEvent, Disposable, MessageItem, Uri, commands } from 'vscode'; +import { workspace, TextDocument, TextDocumentChangeEvent, TextDocumentContentChangeEvent, Disposable } from 'vscode'; import * as Proto from '../protocol'; import { ITypescriptServiceClient } from '../typescriptService'; import { Delayer } from '../utils/async'; -import * as nls from 'vscode-nls'; -let localize = nls.loadMessageBundle(); - interface IDiagnosticRequestor { requestDiagnostic(filepath: string): void; } @@ -98,7 +94,6 @@ export interface Diagnostics { delete(file: string): void; } -const checkTscVersionSettingKey = 'check.tscVersion'; export default class BufferSyncSupport { private readonly client: ITypescriptServiceClient; @@ -111,9 +106,13 @@ export default class BufferSyncSupport { private pendingDiagnostics = new Map(); private readonly diagnosticDelayer: Delayer; - private checkGlobalTSCVersion: boolean; - constructor(client: ITypescriptServiceClient, modeIds: string[], diagnostics: Diagnostics, validate: boolean = true) { + constructor( + client: ITypescriptServiceClient, + modeIds: string[], + diagnostics: Diagnostics, + validate: boolean + ) { this.client = client; this.modeIds = new Set(modeIds); this.diagnostics = diagnostics; @@ -122,9 +121,6 @@ export default class BufferSyncSupport { this.diagnosticDelayer = new Delayer(300); this.syncedBuffers = new Map(); - - const tsConfig = workspace.getConfiguration('typescript'); - this.checkGlobalTSCVersion = client.checkGlobalTSCVersion && this.modeIds.has('typescript') && tsConfig.get(checkTscVersionSettingKey, true); } public listen(): void { @@ -134,10 +130,6 @@ export default class BufferSyncSupport { workspace.textDocuments.forEach(this.onDidOpenTextDocument, this); } - public get validate(): boolean { - return this._validate; - } - public set validate(value: boolean) { this._validate = value; } @@ -174,13 +166,10 @@ export default class BufferSyncSupport { this.syncedBuffers.set(filepath, syncedBuffer); syncedBuffer.open(); this.requestDiagnostic(filepath); - if (document.languageId === 'typescript' || document.languageId === 'typescriptreact') { - this.checkTSCVersion(); - } } private onDidCloseTextDocument(document: TextDocument): void { - let filepath = this.client.normalizePath(document.uri); + const filepath = this.client.normalizePath(document.uri); if (!filepath) { return; } @@ -197,7 +186,7 @@ export default class BufferSyncSupport { } private onDidChangeTextDocument(e: TextDocumentChangeEvent): void { - let filepath = this.client.normalizePath(e.document.uri); + const filepath = this.client.normalizePath(e.document.uri); if (!filepath) { return; } @@ -268,59 +257,4 @@ export default class BufferSyncSupport { } this.pendingDiagnostics.clear(); } - - private checkTSCVersion() { - if (!this.checkGlobalTSCVersion) { - return; - } - this.checkGlobalTSCVersion = false; - - interface MyMessageItem extends MessageItem { - id: number; - } - - let tscVersion: string | undefined = undefined; - try { - let out = cp.execSync('tsc --version', { encoding: 'utf8' }); - if (out) { - let matches = out.trim().match(/Version\s*(.*)$/); - if (matches && matches.length === 2) { - tscVersion = matches[1]; - } - } - } catch (error) { - } - if (tscVersion && tscVersion !== this.client.apiVersion.versionString) { - window.showInformationMessage( - localize('versionMismatch', 'Using TypeScript ({1}) for editor features. TypeScript ({0}) is installed globally on your machine. Errors in VS Code may differ from TSC errors', tscVersion, this.client.apiVersion.versionString), - { - title: localize('moreInformation', 'More Information'), - id: 1 - }, - { - title: localize('doNotCheckAgain', 'Don\'t Check Again'), - id: 2 - }, - { - title: localize('close', 'Close'), - id: 3, - isCloseAffordance: true - } - ).then((selected) => { - if (!selected || selected.id === 3) { - return; - } - switch (selected.id) { - case 1: - commands.executeCommand('vscode.open', Uri.parse('http://go.microsoft.com/fwlink/?LinkId=826239')); - break; - case 2: - const tsConfig = workspace.getConfiguration('typescript'); - tsConfig.update(checkTscVersionSettingKey, false, true); - window.showInformationMessage(localize('updateTscCheck', 'Updated user setting \'typescript.check.tscVersion\' to false')); - break; - } - }); - } - } } \ No newline at end of file diff --git a/extensions/typescript/src/typescriptMain.ts b/extensions/typescript/src/typescriptMain.ts index c285fe7f0b9..692a1da70e1 100644 --- a/extensions/typescript/src/typescriptMain.ts +++ b/extensions/typescript/src/typescriptMain.ts @@ -38,7 +38,7 @@ import BufferSyncSupport from './features/bufferSyncSupport'; import CompletionItemProvider from './features/completionItemProvider'; import WorkspaceSymbolProvider from './features/workspaceSymbolProvider'; import CodeActionProvider from './features/codeActionProvider'; -import RefactorProvider from './features/refactorProvider'; +//import RefactorProvider from './features/refactorProvider'; import ReferenceCodeLensProvider from './features/referencesCodeLensProvider'; import { JsDocCompletionProvider, TryCompleteJsDocCommand } from './features/jsDocCompletionProvider'; import { DirectiveCommentCompletionProvider } from './features/directiveCommentCompletionProvider'; @@ -191,7 +191,7 @@ class LanguageProvider { delete: (file: string) => { this.currentDiagnostics.delete(client.asUrl(file)); } - }); + }, this._validate); this.syntaxDiagnostics = Object.create(null); this.currentDiagnostics = languages.createDiagnosticCollection(description.id); @@ -264,7 +264,7 @@ class LanguageProvider { this.disposables.push(languages.registerRenameProvider(selector, new RenameProvider(client))); this.disposables.push(languages.registerCodeActionsProvider(selector, new CodeActionProvider(client, this.description.id))); - this.disposables.push(languages.registerCodeActionsProvider(selector, new RefactorProvider(client, this.description.id))); + //this.disposables.push(languages.registerCodeActionsProvider(selector, new RefactorProvider(client, this.description.id))); this.registerVersionDependentProviders(); this.description.modeIds.forEach(modeId => { @@ -477,7 +477,9 @@ class TypeScriptServiceClientHost implements ITypescriptServiceClientHost { this.versionStatus = new VersionStatus(); this.disposables.push(this.versionStatus); - this.client = new TypeScriptServiceClient(this, workspaceState, this.versionStatus, plugins, this.disposables); + this.client = new TypeScriptServiceClient(this, workspaceState, this.versionStatus, plugins); + this.disposables.push(this.client); + this.languagePerId = new Map(); for (const description of descriptions) { const manager = new LanguageProvider(this.client, description); diff --git a/extensions/typescript/src/typescriptService.ts b/extensions/typescript/src/typescriptService.ts index ffab7e955a7..9faa0d449e4 100644 --- a/extensions/typescript/src/typescriptService.ts +++ b/extensions/typescript/src/typescriptService.ts @@ -6,7 +6,7 @@ import { CancellationToken, Uri, Event } from 'vscode'; import * as Proto from './protocol'; -import * as semver from 'semver'; +import API from './utils/api'; export interface ITypescriptServiceClientHost { syntaxDiagnosticsReceived(event: Proto.DiagnosticEvent): void; @@ -15,63 +15,6 @@ export interface ITypescriptServiceClientHost { populateService(): void; } -export class API { - - private _version: string; - - constructor(private _versionString: string) { - this._version = semver.valid(_versionString); - if (!this._version) { - this._version = '1.0.0'; - } else { - // Cut of any prerelease tag since we sometimes consume those - // on purpose. - let index = _versionString.indexOf('-'); - if (index >= 0) { - this._version = this._version.substr(0, index); - } - } - } - - public get versionString(): string { - return this._versionString; - } - - public has203Features(): boolean { - return semver.gte(this._version, '2.0.3'); - } - - public has206Features(): boolean { - return semver.gte(this._version, '2.0.6'); - } - - public has208Features(): boolean { - return semver.gte(this._version, '2.0.8'); - } - - public has213Features(): boolean { - return semver.gte(this._version, '2.1.3'); - } - - public has220Features(): boolean { - return semver.gte(this._version, '2.2.0'); - } - - public has222Features(): boolean { - return semver.gte(this._version, '2.2.2'); - } - - public has230Features(): boolean { - return semver.gte(this._version, '2.3.0'); - } - - public has234Features(): boolean { - return semver.gte(this._version, '2.3.4'); - } - public has240Features(): boolean { - return semver.gte(this._version, '2.4.0'); - } -} export interface ITypescriptServiceClient { normalizePath(resource: Uri): string | null; @@ -90,7 +33,6 @@ export interface ITypescriptServiceClient { logTelemetry(eventName: string, properties?: { [prop: string]: string }): void; apiVersion: API; - checkGlobalTSCVersion: boolean; execute(command: 'configure', args: Proto.ConfigureRequestArguments, token?: CancellationToken): Promise; execute(command: 'open', args: Proto.OpenRequestArgs, expectedResult: boolean, token?: CancellationToken): Promise; diff --git a/extensions/typescript/src/typescriptServiceClient.ts b/extensions/typescript/src/typescriptServiceClient.ts index 6ac3e896a6c..4ef43b2cb6c 100644 --- a/extensions/typescript/src/typescriptServiceClient.ts +++ b/extensions/typescript/src/typescriptServiceClient.ts @@ -7,14 +7,13 @@ import * as cp from 'child_process'; import * as path from 'path'; import * as fs from 'fs'; import * as os from 'os'; -import * as net from 'net'; import * as electron from './utils/electron'; import { Reader } from './utils/wireProtocol'; -import { workspace, window, Uri, CancellationToken, Disposable, Memento, MessageItem, QuickPickItem, EventEmitter, Event, commands, WorkspaceConfiguration } from 'vscode'; +import { workspace, window, Uri, CancellationToken, Disposable, Memento, MessageItem, EventEmitter, Event, commands } from 'vscode'; import * as Proto from './protocol'; -import { ITypescriptServiceClient, ITypescriptServiceClientHost, API } from './typescriptService'; +import { ITypescriptServiceClient, ITypescriptServiceClientHost } from './typescriptService'; import { TypeScriptServerPlugin } from './utils/plugins'; import Logger from './utils/logger'; @@ -22,8 +21,12 @@ import VersionStatus from './utils/versionStatus'; import * as is from './utils/is'; import TelemetryReporter from './utils/telemetry'; import Tracer from './utils/tracer'; +import API from "./utils/api"; import * as nls from 'vscode-nls'; +import { TypeScriptServiceConfiguration, TsServerLogLevel } from "./utils/configuration"; +import { TypeScriptVersionProvider } from "./utils/versionProvider"; +import { TypeScriptVersionPicker } from "./utils/versionPicker"; const localize = nls.loadMessageBundle(); interface CallbackItem { @@ -68,120 +71,15 @@ interface RequestItem { callbacks: CallbackItem | null; } -enum TsServerLogLevel { - Off, - Normal, - Terse, - Verbose, -} - -namespace TsServerLogLevel { - export function fromString(value: string): TsServerLogLevel { - switch (value && value.toLowerCase()) { - case 'normal': - return TsServerLogLevel.Normal; - case 'terse': - return TsServerLogLevel.Terse; - case 'verbose': - return TsServerLogLevel.Verbose; - case 'off': - default: - return TsServerLogLevel.Off; - } - } - - export function toString(value: TsServerLogLevel): string { - switch (value) { - case TsServerLogLevel.Normal: - return 'normal'; - case TsServerLogLevel.Terse: - return 'terse'; - case TsServerLogLevel.Verbose: - return 'verbose'; - case TsServerLogLevel.Off: - default: - return 'off'; - } - } -} enum MessageAction { - useLocal, - useBundled, - learnMore, reportIssue } -interface MyQuickPickItem extends QuickPickItem { - id: MessageAction; -} - interface MyMessageItem extends MessageItem { id: MessageAction; } -class TypeScriptServiceConfiguration { - public readonly globalTsdk: string | null; - public readonly localTsdk: string | null; - public readonly npmLocation: string | null; - public readonly tsServerLogLevel: TsServerLogLevel = TsServerLogLevel.Off; - public readonly checkJs: boolean; - - public static loadFromWorkspace(): TypeScriptServiceConfiguration { - return new TypeScriptServiceConfiguration(); - } - - private constructor() { - const configuration = workspace.getConfiguration(); - - this.globalTsdk = TypeScriptServiceConfiguration.extractGlobalTsdk(configuration); - this.localTsdk = TypeScriptServiceConfiguration.extractLocalTsdk(configuration); - this.npmLocation = TypeScriptServiceConfiguration.readNpmLocation(configuration); - this.tsServerLogLevel = TypeScriptServiceConfiguration.readTsServerLogLevel(configuration); - this.checkJs = TypeScriptServiceConfiguration.readCheckJs(configuration); - } - - public isEqualTo(other: TypeScriptServiceConfiguration): boolean { - return this.globalTsdk === other.globalTsdk - && this.localTsdk === other.localTsdk - && this.npmLocation === other.npmLocation - && this.tsServerLogLevel === other.tsServerLogLevel - && this.checkJs === other.checkJs; - } - - private static extractGlobalTsdk(configuration: WorkspaceConfiguration): string | null { - let inspect = configuration.inspect('typescript.tsdk'); - if (inspect && inspect.globalValue && 'string' === typeof inspect.globalValue) { - return inspect.globalValue; - } - if (inspect && inspect.defaultValue && 'string' === typeof inspect.defaultValue) { - return inspect.defaultValue; - } - return null; - } - - private static extractLocalTsdk(configuration: WorkspaceConfiguration): string | null { - let inspect = configuration.inspect('typescript.tsdk'); - if (inspect && inspect.workspaceValue && 'string' === typeof inspect.workspaceValue) { - return inspect.workspaceValue; - } - return null; - } - - private static readTsServerLogLevel(configuration: WorkspaceConfiguration): TsServerLogLevel { - const setting = configuration.get('typescript.tsserver.log', 'off'); - return TsServerLogLevel.fromString(setting); - } - - private static readCheckJs(configuration: WorkspaceConfiguration): boolean { - return configuration.get('javascript.implicitProjectConfig.checkJs', false); - } - - private static readNpmLocation(configuration: WorkspaceConfiguration): string | null { - return configuration.get('typescript.npm', null); - } -} - class RequestQueue { private queue: RequestItem[] = []; private sequenceNumber: number = 0; @@ -218,19 +116,18 @@ class RequestQueue { } } -export default class TypeScriptServiceClient implements ITypescriptServiceClient { - private static useWorkspaceTsdkStorageKey = 'typescript.useWorkspaceTsdk'; - private static tsdkMigratedStorageKey = 'typescript.tsdkMigrated'; +export default class TypeScriptServiceClient implements ITypescriptServiceClient { private static readonly WALK_THROUGH_SNIPPET_SCHEME = 'walkThroughSnippet'; private static readonly WALK_THROUGH_SNIPPET_SCHEME_COLON = `${TypeScriptServiceClient.WALK_THROUGH_SNIPPET_SCHEME}:`; private pathSeparator: string; - private modulePath: string | undefined; private _onReady: { promise: Promise; resolve: () => void; reject: () => void; }; private configuration: TypeScriptServiceConfiguration; - private _checkGlobalTSCVersion: boolean; + private versionProvider: TypeScriptVersionProvider; + private versionPicker: TypeScriptVersionPicker; + private tracer: Tracer; private readonly logger: Logger = new Logger(); private tsServerLogFile: string | null = null; @@ -256,12 +153,13 @@ export default class TypeScriptServiceClient implements ITypescriptServiceClient private _apiVersion: API; private telemetryReporter: TelemetryReporter; + private readonly disposables: Disposable[] = []; + constructor( private readonly host: ITypescriptServiceClientHost, private readonly workspaceState: Memento, private readonly versionStatus: VersionStatus, - private readonly plugins: TypeScriptServerPlugin[], - disposables: Disposable[] + private readonly plugins: TypeScriptServerPlugin[] ) { this.pathSeparator = path.sep; this.lastStart = Date.now(); @@ -279,15 +177,17 @@ export default class TypeScriptServiceClient implements ITypescriptServiceClient this.requestQueue = new RequestQueue(); this.callbacks = new CallbackMap(); this.configuration = TypeScriptServiceConfiguration.loadFromWorkspace(); + this.versionProvider = new TypeScriptVersionProvider(this.configuration); + this.versionPicker = new TypeScriptVersionPicker(this.versionProvider, this.workspaceState); this._apiVersion = new API('1.0.0'); - this._checkGlobalTSCVersion = true; this.tracer = new Tracer(this.logger); - disposables.push(workspace.onDidChangeConfiguration(() => { + this.disposables.push(workspace.onDidChangeConfiguration(() => { const oldConfiguration = this.configuration; this.configuration = TypeScriptServiceConfiguration.loadFromWorkspace(); + this.versionProvider.updateConfiguration(this.configuration); this.tracer.updateConfiguration(); if (this.servicePromise) { @@ -301,10 +201,27 @@ export default class TypeScriptServiceClient implements ITypescriptServiceClient } })); this.telemetryReporter = new TelemetryReporter(); - disposables.push(this.telemetryReporter); + this.disposables.push(this.telemetryReporter); this.startService(); } + public dispose() { + if (this.servicePromise) { + this.servicePromise.then(cp => { + if (cp) { + cp.kill(); + } + }).then(undefined, () => void 0); + } + + while (this.disposables.length) { + const obj = this.disposables.pop(); + if (obj) { + obj.dispose(); + } + } + } + public restartTsServer(): void { const start = () => { this.servicePromise = this.startService(true); @@ -343,10 +260,6 @@ export default class TypeScriptServiceClient implements ITypescriptServiceClient return this._onTypesInstallerInitializationFailed.event; } - public get checkGlobalTSCVersion(): boolean { - return this._checkGlobalTSCVersion; - } - public get apiVersion(): API { return this._apiVersion; } @@ -385,316 +298,145 @@ export default class TypeScriptServiceClient implements ITypescriptServiceClient return Promise.reject(new Error('Could not create TS service')); } - private get bundledTypeScriptPath(): string { - try { - return require.resolve('typescript/lib/tsserver.js'); - } catch (e) { - return ''; - } - } - - private get localTypeScriptPath(): string | null { - const rootPath = this.mainWorkspaceRootPath; - if (!rootPath) { - return null; - } - - if (this.configuration.localTsdk) { - this._checkGlobalTSCVersion = false; - if ((path).isAbsolute(this.configuration.localTsdk)) { - return path.join(this.configuration.localTsdk, 'tsserver.js'); - } - return path.join(rootPath, this.configuration.localTsdk, 'tsserver.js'); - } - - const localModulePath = path.join(rootPath, 'node_modules', 'typescript', 'lib', 'tsserver.js'); - if (fs.existsSync(localModulePath) && this.getTypeScriptVersion(localModulePath)) { - return localModulePath; - } - return null; - } - - private get globalTypescriptPath(): string { - if (this.configuration.globalTsdk) { - this._checkGlobalTSCVersion = false; - if ((path).isAbsolute(this.configuration.globalTsdk)) { - return path.join(this.configuration.globalTsdk, 'tsserver.js'); - } else if (this.mainWorkspaceRootPath) { - return path.join(this.mainWorkspaceRootPath, this.configuration.globalTsdk, 'tsserver.js'); - } - } - - return this.bundledTypeScriptPath; - } - - private hasWorkspaceTsdkSetting(): boolean { - return !!this.configuration.localTsdk; - } - private startService(resendModels: boolean = false): Thenable { - let modulePath: Thenable = Promise.resolve(this.globalTypescriptPath); + let currentVersion = this.versionPicker.currentVersion; - if (!this.workspaceState.get(TypeScriptServiceClient.tsdkMigratedStorageKey, false)) { - this.workspaceState.update(TypeScriptServiceClient.tsdkMigratedStorageKey, true); - if (this.mainWorkspaceRootPath && this.hasWorkspaceTsdkSetting()) { - modulePath = this.showVersionPicker(true); + return this.servicePromise = new Promise((resolve, reject) => { + this.info(`Using tsserver from: ${currentVersion.path}`); + if (!fs.existsSync(currentVersion.path)) { + window.showWarningMessage(localize('noServerFound', 'The path {0} doesn\'t point to a valid tsserver install. Falling back to bundled TypeScript version.', currentVersion.path ? path.dirname(currentVersion.path) : '')); + + this.versionPicker.useBundledVersion(); + currentVersion = this.versionPicker.currentVersion; } - } - return modulePath.then(modulePath => { - if (this.workspaceState.get(TypeScriptServiceClient.useWorkspaceTsdkStorageKey, false)) { + this._apiVersion = this.versionPicker.currentVersion.version; + + const label = this._apiVersion.versionString; + const tooltip = currentVersion.path; + this.versionStatus.showHideStatus(); + this.versionStatus.setInfo(label, tooltip); + + this.requestQueue = new RequestQueue(); + this.callbacks = new CallbackMap(); + this.lastError = null; + + try { + const options: electron.IForkOptions = { + execArgv: [] // [`--debug-brk=5859`] + }; if (this.mainWorkspaceRootPath) { - // TODO: check if we need better error handling - return this.localTypeScriptPath || modulePath; - } - } - return modulePath; - }).then(modulePath => { - return this.getDebugPort().then(debugPort => ({ modulePath, debugPort })); - }).then(({ modulePath, debugPort }) => { - return this.servicePromise = new Promise((resolve, reject) => { - this.info(`Using tsserver from: ${modulePath}`); - if (!fs.existsSync(modulePath)) { - window.showWarningMessage(localize('noServerFound', 'The path {0} doesn\'t point to a valid tsserver install. Falling back to bundled TypeScript version.', modulePath ? path.dirname(modulePath) : '')); - if (!this.bundledTypeScriptPath) { - window.showErrorMessage(localize('noBundledServerFound', 'VSCode\'s tsserver was deleted by another application such as a misbehaving virus detection tool. Please reinstall VS Code.')); - return reject(new Error('Could not find bundled tsserver.js')); - } - modulePath = this.bundledTypeScriptPath; + options.cwd = this.mainWorkspaceRootPath; } - let version = this.getTypeScriptVersion(modulePath); - if (!version) { - version = workspace.getConfiguration().get('typescript.tsdk_version', undefined); + const args: string[] = []; + if (this.apiVersion.has206Features()) { + args.push('--useSingleInferredProject'); + if (workspace.getConfiguration().get('typescript.disableAutomaticTypeAcquisition', false)) { + args.push('--disableAutomaticTypingAcquisition'); + } } - if (version) { - this._apiVersion = new API(version); + if (this.apiVersion.has208Features()) { + args.push('--enableTelemetry'); + } + if (this.apiVersion.has222Features()) { + this.cancellationPipeName = electron.getTempFile(`tscancellation-${electron.makeRandomHexString(20)}`); + args.push('--cancellationPipeName', this.cancellationPipeName + '*'); } - const label = version || localize('versionNumber.custom', 'custom'); - const tooltip = modulePath; - this.modulePath = modulePath; - this.versionStatus.showHideStatus(); - this.versionStatus.setInfo(label, tooltip); + if (this.apiVersion.has222Features()) { + if (this.configuration.tsServerLogLevel !== TsServerLogLevel.Off) { + try { + const logDir = fs.mkdtempSync(path.join(os.tmpdir(), `vscode-tsserver-log-`)); + this.tsServerLogFile = path.join(logDir, `tsserver.log`); + this.info(`TSServer log file: ${this.tsServerLogFile}`); + } catch (e) { + this.error('Could not create TSServer log directory'); + } - - this.requestQueue = new RequestQueue(); - this.callbacks = new CallbackMap(); - this.lastError = null; - - try { - const options: electron.IForkOptions = { - execArgv: [] // [`--debug-brk=5859`] - }; - if (this.mainWorkspaceRootPath) { - options.cwd = this.mainWorkspaceRootPath; - } - - if (debugPort && !isNaN(debugPort)) { - this.info(`TSServer started in debug mode using port ${debugPort}`); - options.execArgv = [`--debug=${debugPort}`]; - } - - const args: string[] = []; - if (this.apiVersion.has206Features()) { - args.push('--useSingleInferredProject'); - if (workspace.getConfiguration().get('typescript.disableAutomaticTypeAcquisition', false)) { - args.push('--disableAutomaticTypingAcquisition'); + if (this.tsServerLogFile) { + args.push('--logVerbosity', TsServerLogLevel.toString(this.configuration.tsServerLogLevel)); + args.push('--logFile', this.tsServerLogFile); } } - if (this.apiVersion.has208Features()) { - args.push('--enableTelemetry'); - } - if (this.apiVersion.has222Features()) { - this.cancellationPipeName = electron.getTempFile(`tscancellation-${electron.makeRandomHexString(20)}`); - args.push('--cancellationPipeName', this.cancellationPipeName + '*'); - } + } - if (this.apiVersion.has222Features()) { - if (this.configuration.tsServerLogLevel !== TsServerLogLevel.Off) { - try { - const logDir = fs.mkdtempSync(path.join(os.tmpdir(), `vscode-tsserver-log-`)); - this.tsServerLogFile = path.join(logDir, `tsserver.log`); - this.info(`TSServer log file: ${this.tsServerLogFile}`); - } catch (e) { - this.error('Could not create TSServer log directory'); - } - - if (this.tsServerLogFile) { - args.push('--logVerbosity', TsServerLogLevel.toString(this.configuration.tsServerLogLevel)); - args.push('--logFile', this.tsServerLogFile); - } + if (this.apiVersion.has230Features()) { + if (this.plugins.length) { + args.push('--globalPlugins', this.plugins.map(x => x.name).join(',')); + if (currentVersion.path === this.versionProvider.defaultVersion.path) { + args.push('--pluginProbeLocations', this.plugins.map(x => x.path).join(',')); } } + } - if (this.apiVersion.has230Features()) { - if (this.plugins.length) { - args.push('--globalPlugins', this.plugins.map(x => x.name).join(',')); - if (modulePath === this.globalTypescriptPath) { - args.push('--pluginProbeLocations', this.plugins.map(x => x.path).join(',')); - } - } + if (this.apiVersion.has234Features()) { + if (this.configuration.npmLocation) { + args.push('--npmLocation', `"${this.configuration.npmLocation}"`); } + } - if (this.apiVersion.has234Features()) { - if (this.configuration.npmLocation) { - args.push('--npmLocation', `"${this.configuration.npmLocation}"`); - } + electron.fork(currentVersion.path, args, options, this.logger, (err: any, childProcess: cp.ChildProcess) => { + if (err) { + this.lastError = err; + this.error('Starting TSServer failed with error.', err); + window.showErrorMessage(localize('serverCouldNotBeStarted', 'TypeScript language server couldn\'t be started. Error message is: {0}', err.message || err)); + this.logTelemetry('error', { message: err.message }); + return; } - - electron.fork(modulePath, args, options, this.logger, (err: any, childProcess: cp.ChildProcess) => { - if (err) { - this.lastError = err; - this.error('Starting TSServer failed with error.', err); - window.showErrorMessage(localize('serverCouldNotBeStarted', 'TypeScript language server couldn\'t be started. Error message is: {0}', err.message || err)); - this.logTelemetry('error', { message: err.message }); - return; + this.lastStart = Date.now(); + childProcess.on('error', (err: Error) => { + this.lastError = err; + this.error('TSServer errored with error.', err); + if (this.tsServerLogFile) { + this.error(`TSServer log file: ${this.tsServerLogFile}`); } - this.lastStart = Date.now(); - childProcess.on('error', (err: Error) => { - this.lastError = err; - this.error('TSServer errored with error.', err); - if (this.tsServerLogFile) { - this.error(`TSServer log file: ${this.tsServerLogFile}`); - } - this.logTelemetry('tsserver.error'); - this.serviceExited(false); - }); - childProcess.on('exit', (code: any) => { - if (code === null || typeof code === 'undefined') { - this.info(`TSServer exited`); - } else { - this.error(`TSServer exited with code: ${code}`); - this.logTelemetry('tsserver.exitWithCode', { code: code }); - } - - if (this.tsServerLogFile) { - this.info(`TSServer log file: ${this.tsServerLogFile}`); - } - this.serviceExited(!this.isRestarting); - this.isRestarting = false; - }); - - this.reader = new Reader( - childProcess.stdout, - (msg) => { this.dispatchMessage(msg); }, - error => { this.error('ReaderError', error); }); - - this._onReady.resolve(); - resolve(childProcess); - this._onTsServerStarted.fire(); - - this.serviceStarted(resendModels); + this.logTelemetry('tsserver.error'); + this.serviceExited(false); }); - } catch (error) { - reject(error); - } - }); + childProcess.on('exit', (code: any) => { + if (code === null || typeof code === 'undefined') { + this.info(`TSServer exited`); + } else { + this.error(`TSServer exited with code: ${code}`); + this.logTelemetry('tsserver.exitWithCode', { code: code }); + } + + if (this.tsServerLogFile) { + this.info(`TSServer log file: ${this.tsServerLogFile}`); + } + this.serviceExited(!this.isRestarting); + this.isRestarting = false; + }); + + this.reader = new Reader( + childProcess.stdout, + (msg) => { this.dispatchMessage(msg); }, + error => { this.error('ReaderError', error); }); + + this._onReady.resolve(); + resolve(childProcess); + this._onTsServerStarted.fire(); + + this.serviceStarted(resendModels); + }); + } catch (error) { + reject(error); + } }); } - private getDebugPort(): Promise { - const value = process.env.TSS_DEBUG; - if (value) { - const port = parseInt(value); - if (!isNaN(port)) { - return Promise.resolve(port); - } - } - - if (workspace.getConfiguration('typescript').get('tsserver.debug', false)) { - return Promise.race([ - new Promise((resolve) => setTimeout(() => resolve(undefined), 1000)), - new Promise((resolve) => { - const server = net.createServer(sock => sock.end()); - server.listen(0, function () { - resolve(server.address().port); - }); - }) - ]); - } - - return Promise.resolve(undefined); - } - - public onVersionStatusClicked(): Thenable { + public onVersionStatusClicked(): Thenable { return this.showVersionPicker(false); } - private showVersionPicker(firstRun: boolean): Thenable { - const modulePath = this.modulePath || this.globalTypescriptPath; - if (!this.mainWorkspaceRootPath || !modulePath) { - return Promise.resolve(modulePath); - } - - const useWorkspaceVersionSetting = this.workspaceState.get(TypeScriptServiceClient.useWorkspaceTsdkStorageKey, false); - const shippedVersion = this.getTypeScriptVersion(this.globalTypescriptPath); - const localModulePath = this.localTypeScriptPath; - - const pickOptions: MyQuickPickItem[] = []; - - pickOptions.push({ - label: localize('useVSCodeVersionOption', 'Use VSCode\'s Version'), - description: shippedVersion || this.globalTypescriptPath, - detail: modulePath === this.globalTypescriptPath && (modulePath !== localModulePath || !useWorkspaceVersionSetting) ? localize('activeVersion', 'Currently active') : '', - id: MessageAction.useBundled, - }); - - if (localModulePath) { - const localVersion = this.getTypeScriptVersion(localModulePath); - pickOptions.push({ - label: localize('useWorkspaceVersionOption', 'Use Workspace Version'), - description: localVersion || localModulePath, - detail: modulePath === localModulePath && (modulePath !== this.globalTypescriptPath || useWorkspaceVersionSetting) ? localize('activeVersion', 'Currently active') : '', - id: MessageAction.useLocal - }); - } - - pickOptions.push({ - label: localize('learnMore', 'Learn More'), - description: '', - id: MessageAction.learnMore - }); - - const tryShowRestart = (newModulePath: string) => { - if (firstRun || newModulePath === this.modulePath) { + private showVersionPicker(firstRun: boolean): Thenable { + return this.versionPicker.show(firstRun).then(change => { + if (firstRun || !change.newVersion || !change.oldVersion || change.oldVersion.path === change.newVersion.path) { return; } this.restartTsServer(); - }; - - return window.showQuickPick(pickOptions, { - placeHolder: localize( - 'selectTsVersion', - 'Select the TypeScript version used for JavaScript and TypeScript language features'), - ignoreFocusOut: firstRun - }) - .then(selected => { - if (!selected) { - return modulePath; - } - switch (selected.id) { - case MessageAction.useLocal: - return this.workspaceState.update(TypeScriptServiceClient.useWorkspaceTsdkStorageKey, true) - .then(_ => { - if (localModulePath) { - tryShowRestart(localModulePath); - } - return localModulePath || ''; - }); - case MessageAction.useBundled: - return this.workspaceState.update(TypeScriptServiceClient.useWorkspaceTsdkStorageKey, false) - .then(_ => { - tryShowRestart(this.globalTypescriptPath); - return this.globalTypescriptPath; - }); - case MessageAction.learnMore: - commands.executeCommand('vscode.open', Uri.parse('https://go.microsoft.com/fwlink/?linkid=839919')); - return modulePath; - default: - return modulePath; - } - }); + }); } public openTsServerLogFile(): Thenable { @@ -779,34 +521,6 @@ export default class TypeScriptServiceClient implements ITypescriptServiceClient }); } - private getTypeScriptVersion(serverPath: string): string | undefined { - if (!fs.existsSync(serverPath)) { - return undefined; - } - - let p = serverPath.split(path.sep); - if (p.length <= 2) { - return undefined; - } - let p2 = p.slice(0, -2); - let modulePath = p2.join(path.sep); - let fileName = path.join(modulePath, 'package.json'); - if (!fs.existsSync(fileName)) { - return undefined; - } - let contents = fs.readFileSync(fileName).toString(); - let desc: any = null; - try { - desc = JSON.parse(contents); - } catch (err) { - return undefined; - } - if (!desc || !desc.version) { - return undefined; - } - return desc.version; - } - private serviceExited(restart: boolean): void { this.servicePromise = null; this.tsServerLogFile = null; @@ -890,7 +604,7 @@ export default class TypeScriptServiceClient implements ITypescriptServiceClient } if (workspace.workspaceFolders && workspace.workspaceFolders.length) { - return workspace.workspaceFolders[0].fsPath; + return workspace.workspaceFolders[0].uri.fsPath; } return undefined; @@ -903,13 +617,13 @@ export default class TypeScriptServiceClient implements ITypescriptServiceClient } if (resource.scheme === 'file' || resource.scheme === 'untitled') { - for (const root of roots.sort((a, b) => a.fsPath.length - b.fsPath.length)) { - if (resource.fsPath.startsWith(root.fsPath)) { - return root.fsPath; + for (const root of roots.sort((a, b) => a.uri.fsPath.length - b.uri.fsPath.length)) { + if (resource.fsPath.startsWith(root.uri.fsPath)) { + return root.uri.fsPath; } } } - return roots[0].fsPath; + return roots[0].uri.fsPath; } public execute(command: string, args: any, expectsResultOrToken?: boolean | CancellationToken): Promise { @@ -1096,4 +810,4 @@ export default class TypeScriptServiceClient implements ITypescriptServiceClient } this.logTelemetry(telemetryData.telemetryEventName, properties); } -} \ No newline at end of file +} diff --git a/extensions/typescript/src/utils/api.ts b/extensions/typescript/src/utils/api.ts new file mode 100644 index 00000000000..b0dd425dc1f --- /dev/null +++ b/extensions/typescript/src/utils/api.ts @@ -0,0 +1,66 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as semver from 'semver'; + +export default class API { + + private readonly _version: string; + + constructor( + private readonly _versionString: string + ) { + this._version = semver.valid(_versionString); + if (!this._version) { + this._version = '1.0.0'; + } else { + // Cut of any prerelease tag since we sometimes consume those + // on purpose. + let index = _versionString.indexOf('-'); + if (index >= 0) { + this._version = this._version.substr(0, index); + } + } + } + + public get versionString(): string { + return this._versionString; + } + + public has203Features(): boolean { + return semver.gte(this._version, '2.0.3'); + } + + public has206Features(): boolean { + return semver.gte(this._version, '2.0.6'); + } + + public has208Features(): boolean { + return semver.gte(this._version, '2.0.8'); + } + + public has213Features(): boolean { + return semver.gte(this._version, '2.1.3'); + } + + public has220Features(): boolean { + return semver.gte(this._version, '2.2.0'); + } + + public has222Features(): boolean { + return semver.gte(this._version, '2.2.2'); + } + + public has230Features(): boolean { + return semver.gte(this._version, '2.3.0'); + } + + public has234Features(): boolean { + return semver.gte(this._version, '2.3.4'); + } + public has240Features(): boolean { + return semver.gte(this._version, '2.4.0'); + } +} \ No newline at end of file diff --git a/extensions/typescript/src/utils/configuration.ts b/extensions/typescript/src/utils/configuration.ts new file mode 100644 index 00000000000..f9d069039a9 --- /dev/null +++ b/extensions/typescript/src/utils/configuration.ts @@ -0,0 +1,104 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { WorkspaceConfiguration, workspace } from "vscode"; + +export enum TsServerLogLevel { + Off, + Normal, + Terse, + Verbose, +} + +export namespace TsServerLogLevel { + export function fromString(value: string): TsServerLogLevel { + switch (value && value.toLowerCase()) { + case 'normal': + return TsServerLogLevel.Normal; + case 'terse': + return TsServerLogLevel.Terse; + case 'verbose': + return TsServerLogLevel.Verbose; + case 'off': + default: + return TsServerLogLevel.Off; + } + } + + export function toString(value: TsServerLogLevel): string { + switch (value) { + case TsServerLogLevel.Normal: + return 'normal'; + case TsServerLogLevel.Terse: + return 'terse'; + case TsServerLogLevel.Verbose: + return 'verbose'; + case TsServerLogLevel.Off: + default: + return 'off'; + } + } +} + +export class TypeScriptServiceConfiguration { + public readonly globalTsdk: string | null; + public readonly localTsdk: string | null; + public readonly npmLocation: string | null; + public readonly tsServerLogLevel: TsServerLogLevel = TsServerLogLevel.Off; + public readonly checkJs: boolean; + + public static loadFromWorkspace(): TypeScriptServiceConfiguration { + return new TypeScriptServiceConfiguration(); + } + + private constructor() { + const configuration = workspace.getConfiguration(); + + this.globalTsdk = TypeScriptServiceConfiguration.extractGlobalTsdk(configuration); + this.localTsdk = TypeScriptServiceConfiguration.extractLocalTsdk(configuration); + this.npmLocation = TypeScriptServiceConfiguration.readNpmLocation(configuration); + this.tsServerLogLevel = TypeScriptServiceConfiguration.readTsServerLogLevel(configuration); + this.checkJs = TypeScriptServiceConfiguration.readCheckJs(configuration); + } + + public isEqualTo(other: TypeScriptServiceConfiguration): boolean { + return this.globalTsdk === other.globalTsdk + && this.localTsdk === other.localTsdk + && this.npmLocation === other.npmLocation + && this.tsServerLogLevel === other.tsServerLogLevel + && this.checkJs === other.checkJs; + } + + private static extractGlobalTsdk(configuration: WorkspaceConfiguration): string | null { + let inspect = configuration.inspect('typescript.tsdk'); + if (inspect && inspect.globalValue && 'string' === typeof inspect.globalValue) { + return inspect.globalValue; + } + if (inspect && inspect.defaultValue && 'string' === typeof inspect.defaultValue) { + return inspect.defaultValue; + } + return null; + } + + private static extractLocalTsdk(configuration: WorkspaceConfiguration): string | null { + let inspect = configuration.inspect('typescript.tsdk'); + if (inspect && inspect.workspaceValue && 'string' === typeof inspect.workspaceValue) { + return inspect.workspaceValue; + } + return null; + } + + private static readTsServerLogLevel(configuration: WorkspaceConfiguration): TsServerLogLevel { + const setting = configuration.get('typescript.tsserver.log', 'off'); + return TsServerLogLevel.fromString(setting); + } + + private static readCheckJs(configuration: WorkspaceConfiguration): boolean { + return configuration.get('javascript.implicitProjectConfig.checkJs', false); + } + + private static readNpmLocation(configuration: WorkspaceConfiguration): string | null { + return configuration.get('typescript.npm', null); + } +} \ No newline at end of file diff --git a/extensions/typescript/src/utils/versionPicker.ts b/extensions/typescript/src/utils/versionPicker.ts new file mode 100644 index 00000000000..279553adf10 --- /dev/null +++ b/extensions/typescript/src/utils/versionPicker.ts @@ -0,0 +1,119 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as nls from 'vscode-nls'; +import { TypeScriptVersionProvider, TypeScriptVersion } from "./versionProvider"; +import { Memento, commands, Uri, window, QuickPickItem } from "vscode"; + +const localize = nls.loadMessageBundle(); + +const useWorkspaceTsdkStorageKey = 'typescript.useWorkspaceTsdk'; + +interface MyQuickPickItem extends QuickPickItem { + id: MessageAction; +} + +enum MessageAction { + useLocal, + useBundled, + learnMore +} + +export class TypeScriptVersionPicker { + private _currentVersion: TypeScriptVersion; + + public constructor( + private readonly versionProvider: TypeScriptVersionProvider, + private readonly workspaceState: Memento + ) { + this._currentVersion = this.versionProvider.defaultVersion; + + if (workspaceState.get(useWorkspaceTsdkStorageKey, false)) { + const localVersion = this.versionProvider.localVersion; + if (localVersion) { + this._currentVersion = localVersion; + } + } + } + + public get currentVersion(): TypeScriptVersion { + return this._currentVersion; + } + + public useBundledVersion(): void { + this._currentVersion = this.versionProvider.bundledVersion; + } + + public show(firstRun?: boolean): Thenable<{ oldVersion?: TypeScriptVersion, newVersion?: TypeScriptVersion }> { + const useWorkspaceVersionSetting = this.workspaceState.get(useWorkspaceTsdkStorageKey, false); + const shippedVersion = this.versionProvider.defaultVersion; + const localVersion = this.versionProvider.localVersion; + + const pickOptions: MyQuickPickItem[] = []; + + pickOptions.push({ + label: localize('useVSCodeVersionOption', 'Use VSCode\'s Version'), + description: shippedVersion.version.versionString, + detail: this.currentVersion.path === shippedVersion.path && (this.currentVersion.path !== (localVersion && localVersion.path) || !useWorkspaceVersionSetting) ? localize('activeVersion', 'Currently active') : '', + id: MessageAction.useBundled + }); + + if (localVersion) { + pickOptions.push({ + label: localize('useWorkspaceVersionOption', 'Use Workspace Version'), + description: localVersion.version.versionString, + detail: this.currentVersion.path === localVersion.path && (this.currentVersion.path !== shippedVersion.path || useWorkspaceVersionSetting) ? localize('activeVersion', 'Currently active') : '', + id: MessageAction.useLocal + }); + } + + pickOptions.push({ + label: localize('learnMore', 'Learn More'), + description: '', + id: MessageAction.learnMore + }); + + return window.showQuickPick(pickOptions, { + placeHolder: localize( + 'selectTsVersion', + 'Select the TypeScript version used for JavaScript and TypeScript language features'), + ignoreFocusOut: firstRun + }) + .then(selected => { + if (!selected) { + return { oldVersion: this.currentVersion }; + } + switch (selected.id) { + case MessageAction.useLocal: + return this.workspaceState.update(useWorkspaceTsdkStorageKey, true) + .then(_ => { + if (localVersion) { + const previousVersion = this.currentVersion; + + this._currentVersion = localVersion; + return { oldVersion: previousVersion, newVersion: localVersion }; + } + return { oldVersion: this.currentVersion }; + }); + + case MessageAction.useBundled: + return this.workspaceState.update(useWorkspaceTsdkStorageKey, false) + .then(_ => { + const previousVersion = this.currentVersion; + this._currentVersion = shippedVersion; + return { oldVersion: previousVersion, newVersion: shippedVersion }; + }); + + case MessageAction.learnMore: + commands.executeCommand('vscode.open', Uri.parse('https://go.microsoft.com/fwlink/?linkid=839919')); + return { oldVersion: this.currentVersion }; + + default: + return { oldVersion: this.currentVersion }; + } + }); + + } +} \ No newline at end of file diff --git a/extensions/typescript/src/utils/versionProvider.ts b/extensions/typescript/src/utils/versionProvider.ts new file mode 100644 index 00000000000..3de78a6bedc --- /dev/null +++ b/extensions/typescript/src/utils/versionProvider.ts @@ -0,0 +1,146 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import * as nls from 'vscode-nls'; +const localize = nls.loadMessageBundle(); + +import * as path from 'path'; +import * as fs from 'fs'; + +import { workspace, window } from "vscode"; + +import { TypeScriptServiceConfiguration } from "./configuration"; +import API from './api'; + + +export interface TypeScriptVersion { + version: API; + path: string; +} + + +export class TypeScriptVersionProvider { + public constructor( + private configuration: TypeScriptServiceConfiguration + ) { } + + public updateConfiguration(configuration: TypeScriptServiceConfiguration): void { + this.configuration = configuration; + } + + public get defaultVersion(): TypeScriptVersion { + return this.globalVersion || this.bundledVersion; + } + + public get globalVersion(): TypeScriptVersion | undefined { + if (this.configuration.globalTsdk) { + const globals = this.getTypeScriptsFromPaths(this.configuration.globalTsdk); + if (globals && globals.length) { + return globals[0]; + } + } + return undefined; + } + + public get localVersion(): TypeScriptVersion | undefined { + const tsdkVersions = this.localTsdkVersions; + if (tsdkVersions && tsdkVersions.length) { + return tsdkVersions[0]; + } + + const nodeVersions = this.localNodeModulesVersions; + if (nodeVersions && nodeVersions.length) { + return nodeVersions[0]; + } + return undefined; + } + + public get bundledVersion(): TypeScriptVersion { + try { + const bundledVersion = this.loadFromPath(require.resolve('typescript/lib/tsserver.js')); + if (bundledVersion) { + return bundledVersion; + } + } catch (e) { + // noop + } + window.showErrorMessage(localize( + 'noBundledServerFound', + 'VSCode\'s tsserver was deleted by another application such as a misbehaving virus detection tool. Please reinstall VS Code.')); + throw new Error('Could not find bundled tsserver.js'); + } + + private get localTsdkVersions(): TypeScriptVersion[] { + return this.configuration.localTsdk + ? this.getTypeScriptsFromPaths(this.configuration.localTsdk) + : []; + } + + private get localNodeModulesVersions(): TypeScriptVersion[] { + return this.getTypeScriptsFromPaths(path.join('node_modules', 'typescript', 'lib')); + } + + private getTypeScriptsFromPaths(typeScriptPath: string): TypeScriptVersion[] { + if (path.isAbsolute(typeScriptPath)) { + const version = this.loadFromPath(path.join(typeScriptPath, 'tsserver.js')); + return version ? [version] : []; + } + + if (!workspace.workspaceFolders) { + return []; + } + + return [workspace.workspaceFolders[0]] + .map(root => path.join(root.uri.fsPath, typeScriptPath, 'tsserver.js')) + .map(path => this.loadFromPath(path)) + .filter(x => !!x) as TypeScriptVersion[]; + } + + public loadFromPath(path: string): TypeScriptVersion | undefined { + if (!fs.existsSync(path)) { + return undefined; + } + + const version = this.getTypeScriptVersion(path); + if (version) { + return { path, version }; + } + + // Allow TS developers to provide custom version + const tsdkVersion = workspace.getConfiguration().get('typescript.tsdk_version', undefined); + if (tsdkVersion) { + return { path, version: new API(tsdkVersion) }; + } + + return undefined; + } + + private getTypeScriptVersion(serverPath: string): API | undefined { + if (!fs.existsSync(serverPath)) { + return undefined; + } + + let p = serverPath.split(path.sep); + if (p.length <= 2) { + return undefined; + } + let p2 = p.slice(0, -2); + let modulePath = p2.join(path.sep); + let fileName = path.join(modulePath, 'package.json'); + if (!fs.existsSync(fileName)) { + return undefined; + } + let contents = fs.readFileSync(fileName).toString(); + let desc: any = null; + try { + desc = JSON.parse(contents); + } catch (err) { + return undefined; + } + if (!desc || !desc.version) { + return undefined; + } + return desc.version ? new API(desc.version) : undefined; + } +} diff --git a/extensions/typescript/syntaxes/TypeScript.tmLanguage.json b/extensions/typescript/syntaxes/TypeScript.tmLanguage.json index 6e16d697d52..96f7d0e6fed 100644 --- a/extensions/typescript/syntaxes/TypeScript.tmLanguage.json +++ b/extensions/typescript/syntaxes/TypeScript.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/Microsoft/TypeScript-TmLanguage/commit/9be58bc51f179fd4119dbd5caaa7693a381a13b5", + "version": "https://github.com/Microsoft/TypeScript-TmLanguage/commit/648a036db2bad78ee93463269ec49ed91ee5aa91", "name": "TypeScript", "scopeName": "source.ts", "fileTypes": [ @@ -100,7 +100,7 @@ "patterns": [ { "name": "meta.var-single-variable.expr.ts", - "begin": "(?x)([_$[:alpha:]][_$[:alnum:]]*)(?=\\s*\n (=\\s*(\n ((async\\s+)?(\n (function\\s*[(<]) |\n (function\\s+) |\n ([_$[:alpha:]][_$[:alnum:]]*\\s*=>)\n )) |\n ((async\\s*)?(\n ([(]\\s*(([)]\\s*:)|([_$[:alpha:]][_$[:alnum:]]*\\s*:)|(\\.\\.\\.) )) |\n ([<]\\s*[_$[:alpha:]][_$[:alnum:]]*((\\s+extends\\s*[^=>])|(\\s*[,]))) |\n ((<([^<>=]|=[^<]|\\<([^=<>]|=[^<])+\\>)+>\\s*)?\\(([^()]|\\([^()]*\\))*\\)(\\s*:\\s*(.)*)?\\s*=>)\n ))\n )) |\n (:\\s*(\n (<) |\n ([(]\\s*(\n ([)]) |\n (\\.\\.\\.) |\n ([_$[:alnum:]]+\\s*(\n ([:,?=])|\n ([)]\\s*=>)\n ))\n ))\n ))\n)", + "begin": "(?x)([_$[:alpha:]][_$[:alnum:]]*)(?=\\s*\n (=\\s*(\n ((async\\s+)?(\n (function\\s*[(<*]) |\n (function\\s+) |\n ([_$[:alpha:]][_$[:alnum:]]*\\s*=>)\n )) |\n ((async\\s*)?(\n ([(]\\s*(([)]\\s*:)|([_$[:alpha:]][_$[:alnum:]]*\\s*:)|(\\.\\.\\.) )) |\n ([<]\\s*[_$[:alpha:]][_$[:alnum:]]*((\\s+extends\\s*[^=>])|(\\s*[,]))) |\n ((<([^<>=]|=[^<]|\\<([^=<>]|=[^<])+\\>)+>\\s*)?\\(([^()]|\\([^()]*\\))*\\)(\\s*:\\s*(.)*)?\\s*=>)\n ))\n )) |\n (:\\s*(\n (<) |\n ([(]\\s*(\n ([)]) |\n (\\.\\.\\.) |\n ([_$[:alnum:]]+\\s*(\n ([:,?=])|\n ([)]\\s*=>)\n ))\n ))\n ))\n)", "beginCaptures": { "1": { "name": "meta.definition.variable.ts entity.name.function.ts" @@ -200,7 +200,7 @@ "include": "#comment" }, { - "begin": "(?=(([_$[:alpha:]][_$[:alnum:]]*)|(\\'.*\\')|(\\\".*\\\")|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])+\\]))\\s*(:))", + "begin": "(?=(([_$[:alpha:]][_$[:alnum:]]*)|(\\'([^\\'\\\\]|\\\\\\'|\\\\)*\\')|(\\\"([^\\\"\\\\]|\\\\\\\"|\\\\)*\\\")|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])+\\]))\\s*(:))", "end": "(?=,|\\})", "patterns": [ { @@ -226,7 +226,7 @@ ] }, "object-binding-element-propertyName": { - "begin": "(?=(([_$[:alpha:]][_$[:alnum:]]*)|(\\'.*\\')|(\\\".*\\\")|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])+\\]))\\s*(:))", + "begin": "(?=(([_$[:alpha:]][_$[:alnum:]]*)|(\\'([^\\'\\\\]|\\\\\\'|\\\\)*\\')|(\\\"([^\\\"\\\\]|\\\\\\\"|\\\\)*\\\")|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])+\\]))\\s*(:))", "end": "(:)", "endCaptures": { "0": { @@ -360,7 +360,7 @@ "include": "#function-expression" }, { - "include": "#class-or-interface-declaration" + "include": "#class-expression" }, { "include": "#arrow-function" @@ -451,7 +451,10 @@ "include": "#function-declaration" }, { - "include": "#class-or-interface-declaration" + "include": "#class-declaration" + }, + { + "include": "#interface-declaration" }, { "include": "#type-declaration" @@ -502,7 +505,7 @@ "name": "entity.name.type.alias.ts" } }, - "end": "(?=[};]|\\bvar\\b|\\blet\\b|\\bconst\\b|\\btype\\b|\\bfunction\\b|\\bclass\\b|\\binterface\\b|\\bnamespace\\b|\\bmodule\\b|\\bimport\\b|\\benum\\b|\\bdeclare\\b|\\bexport\\b|\\babstract\\b|\\basync\\b)", + "end": "(?=[};]|\\babstract\\b|\\basync\\b|\\bclass\\b|\\bconst\\b|\\bdeclare\\b|\\benum\\b|\\bexport\\b|\\bfunction\\b|\\bimport\\b|\\binterface\\b|\\blet\\b|\\bmodule\\b|\\bnamespace\\b|\\btype\\b|\\bvar\\b)", "patterns": [ { "include": "#comment" @@ -580,7 +583,7 @@ ] }, { - "begin": "(?=((\\'.*\\')|(\\\".*\\\")|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])+\\])))", + "begin": "(?=((\\'([^\\'\\\\]|\\\\\\'|\\\\)*\\')|(\\\"([^\\\"\\\\]|\\\\\\\"|\\\\)*\\\")|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])+\\])))", "end": "(?=,|\\}|$)", "patterns": [ { @@ -615,7 +618,7 @@ "name": "storage.type.namespace.ts" } }, - "end": "(?<=\\})", + "end": "(?<=\\})|(?=;|\\babstract\\b|\\basync\\b|\\bclass\\b|\\bconst\\b|\\bdeclare\\b|\\benum\\b|\\bexport\\b|\\bfunction\\b|\\bimport\\b|\\binterface\\b|\\blet\\b|\\bmodule\\b|\\bnamespace\\b|\\btype\\b|\\bvar\\b)", "patterns": [ { "include": "#comment" @@ -692,7 +695,7 @@ "name": "keyword.operator.assignment.ts" } }, - "end": "(?=;|$)", + "end": "(?=;|$|^)", "patterns": [ { "include": "#comment" @@ -727,7 +730,7 @@ "name": "keyword.control.import.ts" } }, - "end": "(?=;|$)", + "end": "(?=;|$|^)", "patterns": [ { "include": "#import-export-declaration" @@ -767,7 +770,7 @@ "name": "keyword.control.default.ts" } }, - "end": "(?=;|\\bexport\\b|\\bfunction\\b|\\bclass\\b|\\binterface\\b|\\blet\\b|\\bvar\\b|\\bconst\\b|\\bimport\\b|\\benum\\b|\\bnamespace\\b|\\bmodule\\b|\\btype\\b|\\babstract\\b|\\bdeclare\\b|\\basync\\b|$)", + "end": "(?=;|$|\\babstract\\b|\\basync\\b|\\bclass\\b|\\bconst\\b|\\bdeclare\\b|\\benum\\b|\\bexport\\b|\\bfunction\\b|\\bimport\\b|\\binterface\\b|\\blet\\b|\\bmodule\\b|\\bnamespace\\b|\\btype\\b|\\bvar\\b)", "patterns": [ { "include": "#expression" @@ -782,7 +785,7 @@ "name": "keyword.control.export.ts" } }, - "end": "(?=;|\\bexport\\b|\\bfunction\\b|\\bclass\\b|\\binterface\\b|\\blet\\b|\\bvar\\b|\\bconst\\b|\\bimport\\b|\\benum\\b|\\bnamespace\\b|\\bmodule\\b|\\btype\\b|\\babstract\\b|\\bdeclare\\b|\\basync\\b|$)", + "end": "(?=;|$|\\babstract\\b|\\basync\\b|\\bclass\\b|\\bconst\\b|\\bdeclare\\b|\\benum\\b|\\bexport\\b|\\bfunction\\b|\\bimport\\b|\\binterface\\b|\\blet\\b|\\bmodule\\b|\\bnamespace\\b|\\btype\\b|\\bvar\\b)", "patterns": [ { "include": "#import-export-declaration" @@ -877,93 +880,101 @@ } ] }, - "class-or-interface-declaration": { + "class-declaration": { + "name": "meta.class.ts", + "begin": "(?)\n )) |\n ((async\\s*)?(\n ([(]\\s*(([)]\\s*:)|([_$[:alpha:]][_$[:alnum:]]*\\s*:)|(\\.\\.\\.) )) |\n ([<]\\s*[_$[:alpha:]][_$[:alnum:]]*((\\s+extends\\s*[^=>])|(\\s*[,]))) |\n ((<([^<>=]|=[^<]|\\<([^=<>]|=[^<])+\\>)+>\\s*)?\\(([^()]|\\([^()]*\\))*\\)(\\s*:\\s*(.)*)?\\s*=>)\n ))\n )) |\n (:\\s*(\n (<) |\n ([(]\\s*(\n ([)]) |\n (\\.\\.\\.) |\n ([_$[:alnum:]]+\\s*(\n ([:,?=])|\n ([)]\\s*=>)\n ))\n ))\n ))\n)" + "match": "(?x)([_$[:alpha:]][_$[:alnum:]]*)(?=(\\?\\s*)?\\s*\n (=\\s*(\n ((async\\s+)?(\n (function\\s*[(<*]) |\n (function\\s+) |\n ([_$[:alpha:]][_$[:alnum:]]*\\s*=>)\n )) |\n ((async\\s*)?(\n ([(]\\s*(([)]\\s*:)|([_$[:alpha:]][_$[:alnum:]]*\\s*:)|(\\.\\.\\.) )) |\n ([<]\\s*[_$[:alpha:]][_$[:alnum:]]*((\\s+extends\\s*[^=>])|(\\s*[,]))) |\n ((<([^<>=]|=[^<]|\\<([^=<>]|=[^<])+\\>)+>\\s*)?\\(([^()]|\\([^()]*\\))*\\)(\\s*:\\s*(.)*)?\\s*=>)\n ))\n )) |\n (:\\s*(\n (<) |\n ([(]\\s*(\n ([)]) |\n (\\.\\.\\.) |\n ([_$[:alnum:]]+\\s*(\n ([:,?=])|\n ([)]\\s*=>)\n ))\n ))\n ))\n)" }, { "name": "meta.definition.property.ts variable.object.property.ts", @@ -1182,7 +1193,7 @@ "patterns": [ { "name": "meta.method.declaration.ts", - "begin": "(?)\n )) |\n ((async\\s*)?(\n ([(]\\s*(([)]\\s*:)|([_$[:alpha:]][_$[:alnum:]]*\\s*:)|(\\.\\.\\.) )) |\n ([<]\\s*[_$[:alpha:]][_$[:alnum:]]*((\\s+extends\\s*[^=>])|(\\s*[,]))) |\n ((<([^<>=]|=[^<]|\\<([^=<>]|=[^<])+\\>)+>\\s*)?\\(([^()]|\\([^()]*\\))*\\)(\\s*:\\s*(.)*)?\\s*=>)\n ))\n )) |\n (:\\s*(\n (<) |\n ([(]\\s*(\n ([)]) |\n (\\.\\.\\.) |\n ([_$[:alnum:]]+\\s*(\n ([:,?=])|\n ([)]\\s*=>)\n ))\n ))\n ))\n)", + "match": "(?x)(?:\\s*\\b(public|private|protected|readonly)\\s+)?(\\.\\.\\.)?\\s*(?)\n )) |\n ((async\\s*)?(\n ([(]\\s*(([)]\\s*:)|([_$[:alpha:]][_$[:alnum:]]*\\s*:)|(\\.\\.\\.) )) |\n ([<]\\s*[_$[:alpha:]][_$[:alnum:]]*((\\s+extends\\s*[^=>])|(\\s*[,]))) |\n ((<([^<>=]|=[^<]|\\<([^=<>]|=[^<])+\\>)+>\\s*)?\\(([^()]|\\([^()]*\\))*\\)(\\s*:\\s*(.)*)?\\s*=>)\n ))\n )) |\n (:\\s*(\n (<) |\n ([(]\\s*(\n ([)]) |\n (\\.\\.\\.) |\n ([_$[:alnum:]]+\\s*(\n ([:,?=])|\n ([)]\\s*=>)\n ))\n ))\n ))\n)", "captures": { "1": { "name": "storage.modifier.ts" @@ -1649,7 +1660,7 @@ "include": "#comment" }, { - "begin": "(?=(([_$[:alpha:]][_$[:alnum:]]*)|(\\'.*\\')|(\\\".*\\\")|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])+\\]))\\s*(:))", + "begin": "(?=(([_$[:alpha:]][_$[:alnum:]]*)|(\\'([^\\'\\\\]|\\\\\\'|\\\\)*\\')|(\\\"([^\\\"\\\\]|\\\\\\\"|\\\\)*\\\")|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])+\\]))\\s*(:))", "end": "(?=,|\\})", "patterns": [ { @@ -2535,7 +2546,7 @@ "include": "#object-identifiers" }, { - "match": "(?x)(?:(\\.)\\s*)?([_$[:alpha:]][_$[:alnum:]]*)(?=\\s*=\\s*(\n ((async\\s+)?(\n (function\\s*[(<]) |\n (function\\s+) |\n ([_$[:alpha:]][_$[:alnum:]]*\\s*=>)\n )) |\n ((async\\s*)?(\n ([(]\\s*(([)]\\s*:)|([_$[:alpha:]][_$[:alnum:]]*\\s*:)|(\\.\\.\\.) )) |\n ([<]\\s*[_$[:alpha:]][_$[:alnum:]]*((\\s+extends\\s*[^=>])|(\\s*[,]))) |\n ((<([^<>=]|=[^<]|\\<([^=<>]|=[^<])+\\>)+>\\s*)?\\(([^()]|\\([^()]*\\))*\\)(\\s*:\\s*(.)*)?\\s*=>)\n ))\n))", + "match": "(?x)(?:(\\.)\\s*)?([_$[:alpha:]][_$[:alnum:]]*)(?=\\s*=\\s*(\n ((async\\s+)?(\n (function\\s*[(<*]) |\n (function\\s+) |\n ([_$[:alpha:]][_$[:alnum:]]*\\s*=>)\n )) |\n ((async\\s*)?(\n ([(]\\s*(([)]\\s*:)|([_$[:alpha:]][_$[:alnum:]]*\\s*:)|(\\.\\.\\.) )) |\n ([<]\\s*[_$[:alpha:]][_$[:alnum:]]*((\\s+extends\\s*[^=>])|(\\s*[,]))) |\n ((<([^<>=]|=[^<]|\\<([^=<>]|=[^<])+\\>)+>\\s*)?\\(([^()]|\\([^()]*\\))*\\)(\\s*:\\s*(.)*)?\\s*=>)\n ))\n))", "captures": { "1": { "name": "punctuation.accessor.ts" @@ -2662,13 +2673,13 @@ "name": "keyword.operator.new.ts" } }, - "end": "(?<=\\))|(?=[;),}]|$|((?)\n )) |\n ((async\\s*)?(\n ([(]\\s*(([)]\\s*:)|([_$[:alpha:]][_$[:alnum:]]*\\s*:)|(\\.\\.\\.) )) |\n ([<]\\s*[_$[:alpha:]][_$[:alnum:]]*((\\s+extends\\s*[^=>])|(\\s*[,]))) |\n ((<([^<>=]|=[^<]|\\<([^=<>]|=[^<])+\\>)+>\\s*)?\\(([^()]|\\([^()]*\\))*\\)(\\s*:\\s*(.)*)?\\s*=>)\n ))\n)))", - "beginCaptures": { + "match": "(?x)(?:([_$[:alpha:]][_$[:alnum:]]*)\\s*(?=:\\s*(\n ((async\\s+)?(\n (function\\s*[(<*]) |\n (function\\s+) |\n ([_$[:alpha:]][_$[:alnum:]]*\\s*=>)\n )) |\n ((async\\s*)?(\n ([(]\\s*(([)]\\s*:)|([_$[:alpha:]][_$[:alnum:]]*\\s*:)|(\\.\\.\\.) )) |\n ([<]\\s*[_$[:alpha:]][_$[:alnum:]]*((\\s+extends\\s*[^=>])|(\\s*[,]))) |\n ((<([^<>=]|=[^<]|\\<([^=<>]|=[^<])+\\>)+>\\s*)?\\(([^()]|\\([^()]*\\))*\\)(\\s*:\\s*(.)*)?\\s*=>)\n ))\n)))", + "captures": { "0": { "name": "meta.object-literal.key.ts" }, "1": { "name": "entity.name.function.ts" - }, - "2": { - "name": "punctuation.separator.key-value.ts" } - }, - "end": "(?=,|\\})", - "patterns": [ - { - "include": "#expression" - } - ] + } }, { "name": "meta.object.member.ts", - "begin": "(?:[_$[:alpha:]][_$[:alnum:]]*)\\s*(:)", - "beginCaptures": { + "match": "(?:[_$[:alpha:]][_$[:alnum:]]*)\\s*(?=:)", + "captures": { "0": { "name": "meta.object-literal.key.ts" - }, - "1": { - "name": "punctuation.separator.key-value.ts" } }, - "end": "(?=,|\\})", - "patterns": [ - { - "include": "#expression" - } - ] + "end": "(?=,|\\})" }, { "name": "meta.object.member.ts", @@ -2795,11 +2772,29 @@ } } }, + { + "include": "#object-member-body" + }, { "include": "#punctuation-comma" } ] }, + "object-member-body": { + "name": "meta.object.member.ts", + "begin": ":", + "beginCaptures": { + "0": { + "name": "meta.object-literal.key.ts punctuation.separator.key-value.ts" + } + }, + "end": "(?=,|\\})", + "patterns": [ + { + "include": "#expression" + } + ] + }, "expression-operators": { "patterns": [ { @@ -3225,7 +3220,7 @@ "patterns": [ { "name": "constant.other.character-class.range.regexp", - "match": "(?:.|(\\\\(?:[0-7]{3}|x\\h\\h|u\\h\\h\\h\\h))|(\\\\c[A-Z])|(\\\\.))\\-(?:[^\\]\\\\]|(\\\\(?:[0-7]{3}|x\\h\\h|u\\h\\h\\h\\h))|(\\\\c[A-Z])|(\\\\.))", + "match": "(?:.|(\\\\(?:[0-7]{3}|x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4}))|(\\\\c[A-Z])|(\\\\.))\\-(?:[^\\]\\\\]|(\\\\(?:[0-7]{3}|x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4}))|(\\\\c[A-Z])|(\\\\.))", "captures": { "1": { "name": "constant.character.numeric.regexp" @@ -3265,7 +3260,7 @@ }, { "name": "constant.character.numeric.regexp", - "match": "\\\\([0-7]{3}|x\\h\\h|u\\h\\h\\h\\h)" + "match": "\\\\([0-7]{3}|x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4})" }, { "name": "constant.character.control.regexp", @@ -3315,7 +3310,7 @@ }, "string-character-escape": { "name": "constant.character.escape.ts", - "match": "\\\\(x\\h{2}|[0-2][0-7]{0,2}|3[0-6][0-7]?|37[0-7]?|[4-7][0-7]?|.|$)" + "match": "\\\\(x[0-9A-Fa-f]{2}|[0-2][0-7]{0,2}|3[0-6][0-7]?|37[0-7]?|[4-7][0-7]?|.|$)" }, "template-substitution-element": { "name": "meta.template.expression.ts", @@ -3502,10 +3497,16 @@ }, { "name": "comment.block.ts", - "begin": "/\\*", + "begin": "(/\\*)(?:\\s*((@)internal)(?=\\s|(\\*/)))?", "beginCaptures": { - "0": { + "1": { "name": "punctuation.definition.comment.ts" + }, + "2": { + "name": "storage.type.internaldeclaration.ts" + }, + "3": { + "name": "punctuation.decorator.internaldeclaration.ts" } }, "end": "\\*/", @@ -3516,13 +3517,22 @@ } }, { - "begin": "(^[ \\t]+)?(//)", + "begin": "(^[ \\t]+)?((//)(?:\\s*((@)internal)(?=\\s|$))?)", "beginCaptures": { "1": { "name": "punctuation.whitespace.comment.leading.ts" }, "2": { - "name": "comment.line.double-slash.ts punctuation.definition.comment.ts" + "name": "comment.line.double-slash.ts" + }, + "3": { + "name": "punctuation.definition.comment.ts" + }, + "4": { + "name": "storage.type.internaldeclaration.ts" + }, + "5": { + "name": "punctuation.decorator.internaldeclaration.ts" } }, "end": "(?=^)", @@ -3532,7 +3542,7 @@ }, "directives": { "name": "comment.line.triple-slash.directive.ts", - "begin": "^(///)\\s*(?=<(reference|amd-dependency|amd-module)(\\s+(path|types|no-default-lib|name)\\s*=\\s*((\\'.*\\')|(\\\".*\\\")))+\\s*/>\\s*$)", + "begin": "^(///)\\s*(?=<(reference|amd-dependency|amd-module)(\\s+(path|types|no-default-lib|name)\\s*=\\s*((\\'([^\\'\\\\]|\\\\\\'|\\\\)*\\')|(\\\"([^\\\"\\\\]|\\\\\\\"|\\\\)*\\\")))+\\s*/>\\s*$)", "beginCaptures": { "1": { "name": "punctuation.definition.comment.ts" @@ -3576,7 +3586,7 @@ "docblock": { "patterns": [ { - "match": "(?x)\n((@)access)\n\\s+\n(private|protected|public)\n\\b", + "match": "(?x)\n((@)(?:access|api))\n\\s+\n(private|protected|public)\n\\b", "captures": { "1": { "name": "storage.type.class.jsdoc" @@ -3700,7 +3710,7 @@ } }, { - "match": "(?x)\n((@)see)\n\\s+\n(?:\n # URL\n (\n (?=https?://)\n (?:[^\\s*]|\\*[^/])+\n )\n |\n # JSDoc namepath\n (\n (?!https?://)\n (?:[^@\\s*/]|\\*[^/])+\n )\n)", + "match": "(?x)\n((@)see)\n\\s+\n(?:\n # URL\n (\n (?!\n # Avoid matching bare URIs (also acceptable as links)\n https?://\n |\n # Avoid matching {@inline tags}; we match those below\n (?:\\[[^\\[\\]]*\\])? # Possible description [preceding]{@tag}\n {@(?:link|linkcode|linkplain|tutorial)\\b\n )\n # Matched namepath\n (?:[^\\s*]|\\*[^/])+\n )\n |\n # JSDoc namepath\n (\n (?!https?://)\n (?:[^@\\s*/]|\\*[^/])+\n )\n)", "captures": { "1": { "name": "storage.type.class.jsdoc" @@ -3792,6 +3802,24 @@ }, { "name": "variable.other.jsdoc", + "match": "(?x)\n(\\[)\\s*\n[\\w$]+\n(?:\n (?:\\[\\])? # Foo[ ].bar properties within an array\n \\. # Foo.Bar namespaced parameter\n [\\w$]+\n)*\n(?:\n \\s*\n (=) # [foo=bar] Default parameter value\n \\s*\n (\n # The inner regexes are to stop the match early at */ and to not stop at escaped quotes\n (?:\n \"(?:(?:\\*(?!/))|(?:\\\\(?!\"))|[^*\\\\])*?\" | # [foo=\"bar\"] Double-quoted\n '(?:(?:\\*(?!/))|(?:\\\\(?!'))|[^*\\\\])*?' | # [foo='bar'] Single-quoted\n \\[ (?:(?:\\*(?!/))|[^*])*? \\] | # [foo=[1,2]] Array literal\n (?:(?:\\*(?!/))|[^*])*? # Everything else\n )*\n )\n)?\n\\s*(?:(\\])((?:[^*\\s]|\\*[^\\s/])+)?|(?=\\*/))", + "captures": { + "1": { + "name": "punctuation.definition.optional-value.begin.bracket.square.jsdoc" + }, + "2": { + "name": "keyword.operator.assignment.jsdoc" + }, + "3": { + "name": "source.embedded.ts" + }, + "4": { + "name": "punctuation.definition.optional-value.end.bracket.square.jsdoc" + }, + "5": { + "name": "invalid.illegal.syntax.jsdoc" + } + }, "begin": "\\[", "end": "\\]|(?=\\*/)", "patterns": [ @@ -3890,7 +3918,7 @@ }, { "name": "storage.type.class.jsdoc", - "match": "(?x) (@) (?:abstract|access|alias|arg|argument|async|attribute|augments|author|beta|borrows|bubbles |callback|chainable|class|classdesc|code|config|const|constant|constructor|constructs|copyright |default|defaultvalue|define|deprecated|desc|description|dict|emits|enum|event|example|exception |exports?|extends|extension(?:_?for)?|external|externs|file|fileoverview|final|fires|for|func |function|global|host|ignore|implements|implicitCast|inherit[Dd]oc|inner|instance|interface|kind |lends|license|listens|main|member|memberof!?|method|mixes|mixins?|modifies|module|name|namespace |noalias|nocollapse|nocompile|nosideeffects|override|overview|package|param|preserve|private|prop |property|protected|public|read[Oo]nly|record|require[ds]|returns?|see|since|static|struct|submodule |summary|suppress|template|this|throws|todo|tutorial|type|typedef|unrestricted|uses|var|variation |version|virtual|writeOnce) \\b", + "match": "(?x) (@) (?:abstract|access|alias|api|arg|argument|async|attribute|augments|author|beta|borrows|bubbles |callback|chainable|class|classdesc|code|config|const|constant|constructor|constructs|copyright |default|defaultvalue|define|deprecated|desc|description|dict|emits|enum|event|example|exception |exports?|extends|extension(?:_?for)?|external|externs|file|fileoverview|final|fires|for|func |function|global|host|ignore|implements|implicitCast|inherit[Dd]oc|inner|instance|interface |internal|kind|lends|license|listens|main|member|memberof!?|method|mixes|mixins?|modifies|module |name|namespace|noalias|nocollapse|nocompile|nosideeffects|override|overview|package|param|preserve |private|prop|property|protected|public|read[Oo]nly|record|require[ds]|returns?|see|since|static |struct|submodule|summary|suppress|template|this|throws|todo|tutorial|type|typedef|unrestricted |uses|var|variation|version|virtual|writeOnce) \\b", "captures": { "1": { "name": "punctuation.definition.block.tag.jsdoc" diff --git a/extensions/typescript/syntaxes/TypeScriptReact.tmLanguage.json b/extensions/typescript/syntaxes/TypeScriptReact.tmLanguage.json index e8423e02c96..2d161f0667a 100644 --- a/extensions/typescript/syntaxes/TypeScriptReact.tmLanguage.json +++ b/extensions/typescript/syntaxes/TypeScriptReact.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/Microsoft/TypeScript-TmLanguage/commit/9be58bc51f179fd4119dbd5caaa7693a381a13b5", + "version": "https://github.com/Microsoft/TypeScript-TmLanguage/commit/648a036db2bad78ee93463269ec49ed91ee5aa91", "name": "TypeScriptReact", "scopeName": "source.tsx", "fileTypes": [ @@ -100,7 +100,7 @@ "patterns": [ { "name": "meta.var-single-variable.expr.tsx", - "begin": "(?x)([_$[:alpha:]][_$[:alnum:]]*)(?=\\s*\n (=\\s*(\n ((async\\s+)?(\n (function\\s*[(<]) |\n (function\\s+) |\n ([_$[:alpha:]][_$[:alnum:]]*\\s*=>)\n )) |\n ((async\\s*)?(\n ([(]\\s*(([)]\\s*:)|([_$[:alpha:]][_$[:alnum:]]*\\s*:)|(\\.\\.\\.) )) |\n ([<]\\s*[_$[:alpha:]][_$[:alnum:]]*((\\s+extends\\s*[^=>])|(\\s*[,]))) |\n ((<([^<>=]|=[^<]|\\<([^=<>]|=[^<])+\\>)+>\\s*)?\\(([^()]|\\([^()]*\\))*\\)(\\s*:\\s*(.)*)?\\s*=>)\n ))\n )) |\n (:\\s*(\n (<) |\n ([(]\\s*(\n ([)]) |\n (\\.\\.\\.) |\n ([_$[:alnum:]]+\\s*(\n ([:,?=])|\n ([)]\\s*=>)\n ))\n ))\n ))\n)", + "begin": "(?x)([_$[:alpha:]][_$[:alnum:]]*)(?=\\s*\n (=\\s*(\n ((async\\s+)?(\n (function\\s*[(<*]) |\n (function\\s+) |\n ([_$[:alpha:]][_$[:alnum:]]*\\s*=>)\n )) |\n ((async\\s*)?(\n ([(]\\s*(([)]\\s*:)|([_$[:alpha:]][_$[:alnum:]]*\\s*:)|(\\.\\.\\.) )) |\n ([<]\\s*[_$[:alpha:]][_$[:alnum:]]*((\\s+extends\\s*[^=>])|(\\s*[,]))) |\n ((<([^<>=]|=[^<]|\\<([^=<>]|=[^<])+\\>)+>\\s*)?\\(([^()]|\\([^()]*\\))*\\)(\\s*:\\s*(.)*)?\\s*=>)\n ))\n )) |\n (:\\s*(\n (<) |\n ([(]\\s*(\n ([)]) |\n (\\.\\.\\.) |\n ([_$[:alnum:]]+\\s*(\n ([:,?=])|\n ([)]\\s*=>)\n ))\n ))\n ))\n)", "beginCaptures": { "1": { "name": "meta.definition.variable.tsx entity.name.function.tsx" @@ -200,7 +200,7 @@ "include": "#comment" }, { - "begin": "(?=(([_$[:alpha:]][_$[:alnum:]]*)|(\\'.*\\')|(\\\".*\\\")|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])+\\]))\\s*(:))", + "begin": "(?=(([_$[:alpha:]][_$[:alnum:]]*)|(\\'([^\\'\\\\]|\\\\\\'|\\\\)*\\')|(\\\"([^\\\"\\\\]|\\\\\\\"|\\\\)*\\\")|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])+\\]))\\s*(:))", "end": "(?=,|\\})", "patterns": [ { @@ -226,7 +226,7 @@ ] }, "object-binding-element-propertyName": { - "begin": "(?=(([_$[:alpha:]][_$[:alnum:]]*)|(\\'.*\\')|(\\\".*\\\")|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])+\\]))\\s*(:))", + "begin": "(?=(([_$[:alpha:]][_$[:alnum:]]*)|(\\'([^\\'\\\\]|\\\\\\'|\\\\)*\\')|(\\\"([^\\\"\\\\]|\\\\\\\"|\\\\)*\\\")|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])+\\]))\\s*(:))", "end": "(:)", "endCaptures": { "0": { @@ -363,7 +363,7 @@ "include": "#function-expression" }, { - "include": "#class-or-interface-declaration" + "include": "#class-expression" }, { "include": "#arrow-function" @@ -454,7 +454,10 @@ "include": "#function-declaration" }, { - "include": "#class-or-interface-declaration" + "include": "#class-declaration" + }, + { + "include": "#interface-declaration" }, { "include": "#type-declaration" @@ -505,7 +508,7 @@ "name": "entity.name.type.alias.tsx" } }, - "end": "(?=[};]|\\bvar\\b|\\blet\\b|\\bconst\\b|\\btype\\b|\\bfunction\\b|\\bclass\\b|\\binterface\\b|\\bnamespace\\b|\\bmodule\\b|\\bimport\\b|\\benum\\b|\\bdeclare\\b|\\bexport\\b|\\babstract\\b|\\basync\\b)", + "end": "(?=[};]|\\babstract\\b|\\basync\\b|\\bclass\\b|\\bconst\\b|\\bdeclare\\b|\\benum\\b|\\bexport\\b|\\bfunction\\b|\\bimport\\b|\\binterface\\b|\\blet\\b|\\bmodule\\b|\\bnamespace\\b|\\btype\\b|\\bvar\\b)", "patterns": [ { "include": "#comment" @@ -583,7 +586,7 @@ ] }, { - "begin": "(?=((\\'.*\\')|(\\\".*\\\")|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])+\\])))", + "begin": "(?=((\\'([^\\'\\\\]|\\\\\\'|\\\\)*\\')|(\\\"([^\\\"\\\\]|\\\\\\\"|\\\\)*\\\")|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])+\\])))", "end": "(?=,|\\}|$)", "patterns": [ { @@ -618,7 +621,7 @@ "name": "storage.type.namespace.tsx" } }, - "end": "(?<=\\})", + "end": "(?<=\\})|(?=;|\\babstract\\b|\\basync\\b|\\bclass\\b|\\bconst\\b|\\bdeclare\\b|\\benum\\b|\\bexport\\b|\\bfunction\\b|\\bimport\\b|\\binterface\\b|\\blet\\b|\\bmodule\\b|\\bnamespace\\b|\\btype\\b|\\bvar\\b)", "patterns": [ { "include": "#comment" @@ -695,7 +698,7 @@ "name": "keyword.operator.assignment.tsx" } }, - "end": "(?=;|$)", + "end": "(?=;|$|^)", "patterns": [ { "include": "#comment" @@ -730,7 +733,7 @@ "name": "keyword.control.import.tsx" } }, - "end": "(?=;|$)", + "end": "(?=;|$|^)", "patterns": [ { "include": "#import-export-declaration" @@ -770,7 +773,7 @@ "name": "keyword.control.default.tsx" } }, - "end": "(?=;|\\bexport\\b|\\bfunction\\b|\\bclass\\b|\\binterface\\b|\\blet\\b|\\bvar\\b|\\bconst\\b|\\bimport\\b|\\benum\\b|\\bnamespace\\b|\\bmodule\\b|\\btype\\b|\\babstract\\b|\\bdeclare\\b|\\basync\\b|$)", + "end": "(?=;|$|\\babstract\\b|\\basync\\b|\\bclass\\b|\\bconst\\b|\\bdeclare\\b|\\benum\\b|\\bexport\\b|\\bfunction\\b|\\bimport\\b|\\binterface\\b|\\blet\\b|\\bmodule\\b|\\bnamespace\\b|\\btype\\b|\\bvar\\b)", "patterns": [ { "include": "#expression" @@ -785,7 +788,7 @@ "name": "keyword.control.export.tsx" } }, - "end": "(?=;|\\bexport\\b|\\bfunction\\b|\\bclass\\b|\\binterface\\b|\\blet\\b|\\bvar\\b|\\bconst\\b|\\bimport\\b|\\benum\\b|\\bnamespace\\b|\\bmodule\\b|\\btype\\b|\\babstract\\b|\\bdeclare\\b|\\basync\\b|$)", + "end": "(?=;|$|\\babstract\\b|\\basync\\b|\\bclass\\b|\\bconst\\b|\\bdeclare\\b|\\benum\\b|\\bexport\\b|\\bfunction\\b|\\bimport\\b|\\binterface\\b|\\blet\\b|\\bmodule\\b|\\bnamespace\\b|\\btype\\b|\\bvar\\b)", "patterns": [ { "include": "#import-export-declaration" @@ -880,93 +883,101 @@ } ] }, - "class-or-interface-declaration": { + "class-declaration": { + "name": "meta.class.tsx", + "begin": "(?)\n )) |\n ((async\\s*)?(\n ([(]\\s*(([)]\\s*:)|([_$[:alpha:]][_$[:alnum:]]*\\s*:)|(\\.\\.\\.) )) |\n ([<]\\s*[_$[:alpha:]][_$[:alnum:]]*((\\s+extends\\s*[^=>])|(\\s*[,]))) |\n ((<([^<>=]|=[^<]|\\<([^=<>]|=[^<])+\\>)+>\\s*)?\\(([^()]|\\([^()]*\\))*\\)(\\s*:\\s*(.)*)?\\s*=>)\n ))\n )) |\n (:\\s*(\n (<) |\n ([(]\\s*(\n ([)]) |\n (\\.\\.\\.) |\n ([_$[:alnum:]]+\\s*(\n ([:,?=])|\n ([)]\\s*=>)\n ))\n ))\n ))\n)" + "match": "(?x)([_$[:alpha:]][_$[:alnum:]]*)(?=(\\?\\s*)?\\s*\n (=\\s*(\n ((async\\s+)?(\n (function\\s*[(<*]) |\n (function\\s+) |\n ([_$[:alpha:]][_$[:alnum:]]*\\s*=>)\n )) |\n ((async\\s*)?(\n ([(]\\s*(([)]\\s*:)|([_$[:alpha:]][_$[:alnum:]]*\\s*:)|(\\.\\.\\.) )) |\n ([<]\\s*[_$[:alpha:]][_$[:alnum:]]*((\\s+extends\\s*[^=>])|(\\s*[,]))) |\n ((<([^<>=]|=[^<]|\\<([^=<>]|=[^<])+\\>)+>\\s*)?\\(([^()]|\\([^()]*\\))*\\)(\\s*:\\s*(.)*)?\\s*=>)\n ))\n )) |\n (:\\s*(\n (<) |\n ([(]\\s*(\n ([)]) |\n (\\.\\.\\.) |\n ([_$[:alnum:]]+\\s*(\n ([:,?=])|\n ([)]\\s*=>)\n ))\n ))\n ))\n)" }, { "name": "meta.definition.property.tsx variable.object.property.tsx", @@ -1185,7 +1196,7 @@ "patterns": [ { "name": "meta.method.declaration.tsx", - "begin": "(?)\n )) |\n ((async\\s*)?(\n ([(]\\s*(([)]\\s*:)|([_$[:alpha:]][_$[:alnum:]]*\\s*:)|(\\.\\.\\.) )) |\n ([<]\\s*[_$[:alpha:]][_$[:alnum:]]*((\\s+extends\\s*[^=>])|(\\s*[,]))) |\n ((<([^<>=]|=[^<]|\\<([^=<>]|=[^<])+\\>)+>\\s*)?\\(([^()]|\\([^()]*\\))*\\)(\\s*:\\s*(.)*)?\\s*=>)\n ))\n )) |\n (:\\s*(\n (<) |\n ([(]\\s*(\n ([)]) |\n (\\.\\.\\.) |\n ([_$[:alnum:]]+\\s*(\n ([:,?=])|\n ([)]\\s*=>)\n ))\n ))\n ))\n)", + "match": "(?x)(?:\\s*\\b(public|private|protected|readonly)\\s+)?(\\.\\.\\.)?\\s*(?)\n )) |\n ((async\\s*)?(\n ([(]\\s*(([)]\\s*:)|([_$[:alpha:]][_$[:alnum:]]*\\s*:)|(\\.\\.\\.) )) |\n ([<]\\s*[_$[:alpha:]][_$[:alnum:]]*((\\s+extends\\s*[^=>])|(\\s*[,]))) |\n ((<([^<>=]|=[^<]|\\<([^=<>]|=[^<])+\\>)+>\\s*)?\\(([^()]|\\([^()]*\\))*\\)(\\s*:\\s*(.)*)?\\s*=>)\n ))\n )) |\n (:\\s*(\n (<) |\n ([(]\\s*(\n ([)]) |\n (\\.\\.\\.) |\n ([_$[:alnum:]]+\\s*(\n ([:,?=])|\n ([)]\\s*=>)\n ))\n ))\n ))\n)", "captures": { "1": { "name": "storage.modifier.tsx" @@ -1652,7 +1663,7 @@ "include": "#comment" }, { - "begin": "(?=(([_$[:alpha:]][_$[:alnum:]]*)|(\\'.*\\')|(\\\".*\\\")|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])+\\]))\\s*(:))", + "begin": "(?=(([_$[:alpha:]][_$[:alnum:]]*)|(\\'([^\\'\\\\]|\\\\\\'|\\\\)*\\')|(\\\"([^\\\"\\\\]|\\\\\\\"|\\\\)*\\\")|(\\[([^\\[\\]]|\\[[^\\[\\]]*\\])+\\]))\\s*(:))", "end": "(?=,|\\})", "patterns": [ { @@ -2538,7 +2549,7 @@ "include": "#object-identifiers" }, { - "match": "(?x)(?:(\\.)\\s*)?([_$[:alpha:]][_$[:alnum:]]*)(?=\\s*=\\s*(\n ((async\\s+)?(\n (function\\s*[(<]) |\n (function\\s+) |\n ([_$[:alpha:]][_$[:alnum:]]*\\s*=>)\n )) |\n ((async\\s*)?(\n ([(]\\s*(([)]\\s*:)|([_$[:alpha:]][_$[:alnum:]]*\\s*:)|(\\.\\.\\.) )) |\n ([<]\\s*[_$[:alpha:]][_$[:alnum:]]*((\\s+extends\\s*[^=>])|(\\s*[,]))) |\n ((<([^<>=]|=[^<]|\\<([^=<>]|=[^<])+\\>)+>\\s*)?\\(([^()]|\\([^()]*\\))*\\)(\\s*:\\s*(.)*)?\\s*=>)\n ))\n))", + "match": "(?x)(?:(\\.)\\s*)?([_$[:alpha:]][_$[:alnum:]]*)(?=\\s*=\\s*(\n ((async\\s+)?(\n (function\\s*[(<*]) |\n (function\\s+) |\n ([_$[:alpha:]][_$[:alnum:]]*\\s*=>)\n )) |\n ((async\\s*)?(\n ([(]\\s*(([)]\\s*:)|([_$[:alpha:]][_$[:alnum:]]*\\s*:)|(\\.\\.\\.) )) |\n ([<]\\s*[_$[:alpha:]][_$[:alnum:]]*((\\s+extends\\s*[^=>])|(\\s*[,]))) |\n ((<([^<>=]|=[^<]|\\<([^=<>]|=[^<])+\\>)+>\\s*)?\\(([^()]|\\([^()]*\\))*\\)(\\s*:\\s*(.)*)?\\s*=>)\n ))\n))", "captures": { "1": { "name": "punctuation.accessor.tsx" @@ -2628,13 +2639,13 @@ "name": "keyword.operator.new.tsx" } }, - "end": "(?<=\\))|(?=[;),}]|$|((?)\n )) |\n ((async\\s*)?(\n ([(]\\s*(([)]\\s*:)|([_$[:alpha:]][_$[:alnum:]]*\\s*:)|(\\.\\.\\.) )) |\n ([<]\\s*[_$[:alpha:]][_$[:alnum:]]*((\\s+extends\\s*[^=>])|(\\s*[,]))) |\n ((<([^<>=]|=[^<]|\\<([^=<>]|=[^<])+\\>)+>\\s*)?\\(([^()]|\\([^()]*\\))*\\)(\\s*:\\s*(.)*)?\\s*=>)\n ))\n)))", - "beginCaptures": { + "match": "(?x)(?:([_$[:alpha:]][_$[:alnum:]]*)\\s*(?=:\\s*(\n ((async\\s+)?(\n (function\\s*[(<*]) |\n (function\\s+) |\n ([_$[:alpha:]][_$[:alnum:]]*\\s*=>)\n )) |\n ((async\\s*)?(\n ([(]\\s*(([)]\\s*:)|([_$[:alpha:]][_$[:alnum:]]*\\s*:)|(\\.\\.\\.) )) |\n ([<]\\s*[_$[:alpha:]][_$[:alnum:]]*((\\s+extends\\s*[^=>])|(\\s*[,]))) |\n ((<([^<>=]|=[^<]|\\<([^=<>]|=[^<])+\\>)+>\\s*)?\\(([^()]|\\([^()]*\\))*\\)(\\s*:\\s*(.)*)?\\s*=>)\n ))\n)))", + "captures": { "0": { "name": "meta.object-literal.key.tsx" }, "1": { "name": "entity.name.function.tsx" - }, - "2": { - "name": "punctuation.separator.key-value.tsx" } - }, - "end": "(?=,|\\})", - "patterns": [ - { - "include": "#expression" - } - ] + } }, { "name": "meta.object.member.tsx", - "begin": "(?:[_$[:alpha:]][_$[:alnum:]]*)\\s*(:)", - "beginCaptures": { + "match": "(?:[_$[:alpha:]][_$[:alnum:]]*)\\s*(?=:)", + "captures": { "0": { "name": "meta.object-literal.key.tsx" - }, - "1": { - "name": "punctuation.separator.key-value.tsx" } }, - "end": "(?=,|\\})", - "patterns": [ - { - "include": "#expression" - } - ] + "end": "(?=,|\\})" }, { "name": "meta.object.member.tsx", @@ -2761,11 +2738,29 @@ } } }, + { + "include": "#object-member-body" + }, { "include": "#punctuation-comma" } ] }, + "object-member-body": { + "name": "meta.object.member.tsx", + "begin": ":", + "beginCaptures": { + "0": { + "name": "meta.object-literal.key.tsx punctuation.separator.key-value.tsx" + } + }, + "end": "(?=,|\\})", + "patterns": [ + { + "include": "#expression" + } + ] + }, "expression-operators": { "patterns": [ { @@ -3191,7 +3186,7 @@ "patterns": [ { "name": "constant.other.character-class.range.regexp", - "match": "(?:.|(\\\\(?:[0-7]{3}|x\\h\\h|u\\h\\h\\h\\h))|(\\\\c[A-Z])|(\\\\.))\\-(?:[^\\]\\\\]|(\\\\(?:[0-7]{3}|x\\h\\h|u\\h\\h\\h\\h))|(\\\\c[A-Z])|(\\\\.))", + "match": "(?:.|(\\\\(?:[0-7]{3}|x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4}))|(\\\\c[A-Z])|(\\\\.))\\-(?:[^\\]\\\\]|(\\\\(?:[0-7]{3}|x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4}))|(\\\\c[A-Z])|(\\\\.))", "captures": { "1": { "name": "constant.character.numeric.regexp" @@ -3231,7 +3226,7 @@ }, { "name": "constant.character.numeric.regexp", - "match": "\\\\([0-7]{3}|x\\h\\h|u\\h\\h\\h\\h)" + "match": "\\\\([0-7]{3}|x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4})" }, { "name": "constant.character.control.regexp", @@ -3281,7 +3276,7 @@ }, "string-character-escape": { "name": "constant.character.escape.tsx", - "match": "\\\\(x\\h{2}|[0-2][0-7]{0,2}|3[0-6][0-7]?|37[0-7]?|[4-7][0-7]?|.|$)" + "match": "\\\\(x[0-9A-Fa-f]{2}|[0-2][0-7]{0,2}|3[0-6][0-7]?|37[0-7]?|[4-7][0-7]?|.|$)" }, "template-substitution-element": { "name": "meta.template.expression.tsx", @@ -3468,10 +3463,16 @@ }, { "name": "comment.block.tsx", - "begin": "/\\*", + "begin": "(/\\*)(?:\\s*((@)internal)(?=\\s|(\\*/)))?", "beginCaptures": { - "0": { + "1": { "name": "punctuation.definition.comment.tsx" + }, + "2": { + "name": "storage.type.internaldeclaration.tsx" + }, + "3": { + "name": "punctuation.decorator.internaldeclaration.tsx" } }, "end": "\\*/", @@ -3482,13 +3483,22 @@ } }, { - "begin": "(^[ \\t]+)?(//)", + "begin": "(^[ \\t]+)?((//)(?:\\s*((@)internal)(?=\\s|$))?)", "beginCaptures": { "1": { "name": "punctuation.whitespace.comment.leading.tsx" }, "2": { - "name": "comment.line.double-slash.tsx punctuation.definition.comment.tsx" + "name": "comment.line.double-slash.tsx" + }, + "3": { + "name": "punctuation.definition.comment.tsx" + }, + "4": { + "name": "storage.type.internaldeclaration.tsx" + }, + "5": { + "name": "punctuation.decorator.internaldeclaration.tsx" } }, "end": "(?=^)", @@ -3498,7 +3508,7 @@ }, "directives": { "name": "comment.line.triple-slash.directive.tsx", - "begin": "^(///)\\s*(?=<(reference|amd-dependency|amd-module)(\\s+(path|types|no-default-lib|name)\\s*=\\s*((\\'.*\\')|(\\\".*\\\")))+\\s*/>\\s*$)", + "begin": "^(///)\\s*(?=<(reference|amd-dependency|amd-module)(\\s+(path|types|no-default-lib|name)\\s*=\\s*((\\'([^\\'\\\\]|\\\\\\'|\\\\)*\\')|(\\\"([^\\\"\\\\]|\\\\\\\"|\\\\)*\\\")))+\\s*/>\\s*$)", "beginCaptures": { "1": { "name": "punctuation.definition.comment.tsx" @@ -3542,7 +3552,7 @@ "docblock": { "patterns": [ { - "match": "(?x)\n((@)access)\n\\s+\n(private|protected|public)\n\\b", + "match": "(?x)\n((@)(?:access|api))\n\\s+\n(private|protected|public)\n\\b", "captures": { "1": { "name": "storage.type.class.jsdoc" @@ -3666,7 +3676,7 @@ } }, { - "match": "(?x)\n((@)see)\n\\s+\n(?:\n # URL\n (\n (?=https?://)\n (?:[^\\s*]|\\*[^/])+\n )\n |\n # JSDoc namepath\n (\n (?!https?://)\n (?:[^@\\s*/]|\\*[^/])+\n )\n)", + "match": "(?x)\n((@)see)\n\\s+\n(?:\n # URL\n (\n (?!\n # Avoid matching bare URIs (also acceptable as links)\n https?://\n |\n # Avoid matching {@inline tags}; we match those below\n (?:\\[[^\\[\\]]*\\])? # Possible description [preceding]{@tag}\n {@(?:link|linkcode|linkplain|tutorial)\\b\n )\n # Matched namepath\n (?:[^\\s*]|\\*[^/])+\n )\n |\n # JSDoc namepath\n (\n (?!https?://)\n (?:[^@\\s*/]|\\*[^/])+\n )\n)", "captures": { "1": { "name": "storage.type.class.jsdoc" @@ -3758,6 +3768,24 @@ }, { "name": "variable.other.jsdoc", + "match": "(?x)\n(\\[)\\s*\n[\\w$]+\n(?:\n (?:\\[\\])? # Foo[ ].bar properties within an array\n \\. # Foo.Bar namespaced parameter\n [\\w$]+\n)*\n(?:\n \\s*\n (=) # [foo=bar] Default parameter value\n \\s*\n (\n # The inner regexes are to stop the match early at */ and to not stop at escaped quotes\n (?:\n \"(?:(?:\\*(?!/))|(?:\\\\(?!\"))|[^*\\\\])*?\" | # [foo=\"bar\"] Double-quoted\n '(?:(?:\\*(?!/))|(?:\\\\(?!'))|[^*\\\\])*?' | # [foo='bar'] Single-quoted\n \\[ (?:(?:\\*(?!/))|[^*])*? \\] | # [foo=[1,2]] Array literal\n (?:(?:\\*(?!/))|[^*])*? # Everything else\n )*\n )\n)?\n\\s*(?:(\\])((?:[^*\\s]|\\*[^\\s/])+)?|(?=\\*/))", + "captures": { + "1": { + "name": "punctuation.definition.optional-value.begin.bracket.square.jsdoc" + }, + "2": { + "name": "keyword.operator.assignment.jsdoc" + }, + "3": { + "name": "source.embedded.tsx" + }, + "4": { + "name": "punctuation.definition.optional-value.end.bracket.square.jsdoc" + }, + "5": { + "name": "invalid.illegal.syntax.jsdoc" + } + }, "begin": "\\[", "end": "\\]|(?=\\*/)", "patterns": [ @@ -3856,7 +3884,7 @@ }, { "name": "storage.type.class.jsdoc", - "match": "(?x) (@) (?:abstract|access|alias|arg|argument|async|attribute|augments|author|beta|borrows|bubbles |callback|chainable|class|classdesc|code|config|const|constant|constructor|constructs|copyright |default|defaultvalue|define|deprecated|desc|description|dict|emits|enum|event|example|exception |exports?|extends|extension(?:_?for)?|external|externs|file|fileoverview|final|fires|for|func |function|global|host|ignore|implements|implicitCast|inherit[Dd]oc|inner|instance|interface|kind |lends|license|listens|main|member|memberof!?|method|mixes|mixins?|modifies|module|name|namespace |noalias|nocollapse|nocompile|nosideeffects|override|overview|package|param|preserve|private|prop |property|protected|public|read[Oo]nly|record|require[ds]|returns?|see|since|static|struct|submodule |summary|suppress|template|this|throws|todo|tutorial|type|typedef|unrestricted|uses|var|variation |version|virtual|writeOnce) \\b", + "match": "(?x) (@) (?:abstract|access|alias|api|arg|argument|async|attribute|augments|author|beta|borrows|bubbles |callback|chainable|class|classdesc|code|config|const|constant|constructor|constructs|copyright |default|defaultvalue|define|deprecated|desc|description|dict|emits|enum|event|example|exception |exports?|extends|extension(?:_?for)?|external|externs|file|fileoverview|final|fires|for|func |function|global|host|ignore|implements|implicitCast|inherit[Dd]oc|inner|instance|interface |internal|kind|lends|license|listens|main|member|memberof!?|method|mixes|mixins?|modifies|module |name|namespace|noalias|nocollapse|nocompile|nosideeffects|override|overview|package|param|preserve |private|prop|property|protected|public|read[Oo]nly|record|require[ds]|returns?|see|since|static |struct|submodule|summary|suppress|template|this|throws|todo|tutorial|type|typedef|unrestricted |uses|var|variation|version|virtual|writeOnce) \\b", "captures": { "1": { "name": "punctuation.definition.block.tag.jsdoc" @@ -4122,6 +4150,15 @@ "name": "invalid.illegal.attribute.tsx", "match": "\\S+" }, + "jsx-tag-without-attributes-in-expression": { + "begin": "(?x)\n (?<=[({\\[,?=>:*]|&&|\\|\\||\\?|\\Wreturn|^return|\\Wdefault|^)\\s*\n (?=(<)\\s*((?:[a-z][a-z0-9]*|([_$a-zA-Z][-$\\w.]*))(?))", + "end": "(?!\\s*(<)\\s*((?:[a-z][a-z0-9]*|([_$a-zA-Z][-$\\w.]*))(?))", + "patterns": [ + { + "include": "#jsx-tag-without-attributes" + } + ] + }, "jsx-tag-without-attributes": { "name": "meta.tag.without-attributes.tsx", "begin": "(<)\\s*((?:[a-z][a-z0-9]*|([_$a-zA-Z][-$\\w.]*))(?)", @@ -4162,7 +4199,7 @@ ] }, "jsx-tag-in-expression": { - "begin": "(?x)\n (?<=[({\\[,?=>:*]|&&|\\|\\||\\?|\\Wreturn|^return|\\Wdefault|^)\\s*\n (?!(<)\\s*([_$a-zA-Z][-$\\w.]*(?)) #look ahead is not start of tag without attributes\n (?!<\\s*[_$[:alpha:]][_$[:alnum:]]*((\\s+extends\\s+[^=>])|,)) # look ahead is not type parameter of arrow\n (?=(<)\\s*\n ([_$a-zA-Z][-$\\w.]*(?))", + "begin": "(?x)\n (?<=[({\\[,?=>:*]|&&|\\|\\||\\?|\\Wreturn|^return|\\Wdefault|^)\\s*\n (?!<\\s*[_$[:alpha:]][_$[:alnum:]]*((\\s+extends\\s+[^=>])|,)) # look ahead is not type parameter of arrow\n (?=(<)\\s*\n ([_$a-zA-Z][-$\\w.]*(?))", "end": "(/>)|(?:())", "endCaptures": { "0": { @@ -4293,7 +4330,7 @@ "jsx": { "patterns": [ { - "include": "#jsx-tag-without-attributes" + "include": "#jsx-tag-without-attributes-in-expression" }, { "include": "#jsx-tag-in-expression" diff --git a/extensions/vscode-api-tests/src/index.ts b/extensions/vscode-api-tests/src/index.ts index a489e4c880a..f65a756a8de 100644 --- a/extensions/vscode-api-tests/src/index.ts +++ b/extensions/vscode-api-tests/src/index.ts @@ -22,7 +22,7 @@ const testRunner = require('vscode/lib/testrunner'); testRunner.configure({ ui: 'tdd', // the TDD UI is being used in extension.test.ts (suite, test, etc.) useColors: process.platform !== 'win32', // colored output from test results (only windows cannot handle) - timeout: 10000 + timeout: 60000 }); export = testRunner; diff --git a/extensions/vscode-colorize-tests/src/index.ts b/extensions/vscode-colorize-tests/src/index.ts index 984d15dafc5..da1cb0f2c42 100644 --- a/extensions/vscode-colorize-tests/src/index.ts +++ b/extensions/vscode-colorize-tests/src/index.ts @@ -22,7 +22,7 @@ const testRunner = require('vscode/lib/testrunner'); testRunner.configure({ ui: 'tdd', // the TDD UI is being used in extension.test.ts (suite, test, etc.) useColors: process.platform !== 'win32', // colored output from test results (only windows cannot handle) - timeout: 10000 + timeout: 60000 }); export = testRunner; \ No newline at end of file diff --git a/package.json b/package.json index b004e23178f..8d380a7370b 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "code-oss-dev", "version": "1.15.0", "electronVersion": "1.6.6", - "distro": "3d527860a13c6374d293f95ac2b8d26338d5f428", + "distro": "4bec0ed99ee450bfbefa4baf2433c6a032e88bd4", "author": { "name": "Microsoft Corporation" }, @@ -83,7 +83,7 @@ "gulp-replace": "^0.5.4", "gulp-shell": "^0.5.2", "gulp-sourcemaps": "^1.11.0", - "gulp-tsb": "^2.0.3", + "gulp-tsb": "^2.0.4", "gulp-tslint": "^7.0.1", "gulp-uglify": "^3.0.0", "gulp-util": "^3.0.6", @@ -107,7 +107,7 @@ "sinon": "^1.17.2", "source-map": "^0.4.4", "tslint": "^4.3.1", - "typescript": "2.3.3", + "typescript": "2.4.1", "typescript-formatter": "4.0.1", "uglify-es": "^3.0.18", "underscore": "^1.8.2", diff --git a/resources/letterpress-dark.svg b/resources/letterpress-dark.svg new file mode 100644 index 00000000000..5d6fca4300c --- /dev/null +++ b/resources/letterpress-dark.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/letterpress-hc.svg b/resources/letterpress-hc.svg new file mode 100644 index 00000000000..94cbfbd81e3 --- /dev/null +++ b/resources/letterpress-hc.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/letterpress.svg b/resources/letterpress.svg new file mode 100644 index 00000000000..86f01932099 --- /dev/null +++ b/resources/letterpress.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/vs/base/browser/htmlContentRenderer.ts b/src/vs/base/browser/htmlContentRenderer.ts index 8f6e459c75d..dda7d0788a5 100644 --- a/src/vs/base/browser/htmlContentRenderer.ts +++ b/src/vs/base/browser/htmlContentRenderer.ts @@ -126,7 +126,7 @@ export function renderMarkdown(markdown: string, options: RenderOptions = {}): N if (options.codeBlockRenderer) { renderer.code = (code, lang) => { - let value = options.codeBlockRenderer(lang, code); + const value = options.codeBlockRenderer(lang, code); if (typeof value === 'string') { return value; } @@ -136,15 +136,15 @@ export function renderMarkdown(markdown: string, options: RenderOptions = {}): N // but update the node with the real result later. const id = defaultGenerator.nextId(); TPromise.join([value, withInnerHTML]).done(values => { - let strValue = values[0] as string; - let span = element.querySelector(`span[data-code="${id}"]`); + const strValue = values[0] as string; + const span = element.querySelector(`div[data-code="${id}"]`); if (span) { span.innerHTML = strValue; } }, err => { // ignore }); - return `${escape(code)}`; + return `
${escape(code)}
`; } return code; diff --git a/src/vs/base/common/uri.ts b/src/vs/base/common/uri.ts index 3de56c4ab75..a7a15341ed7 100644 --- a/src/vs/base/common/uri.ts +++ b/src/vs/base/common/uri.ts @@ -218,7 +218,7 @@ export default class URI { ret._scheme = 'file'; // normalize to fwd-slashes on windows, - // on other systems bwd-slaches are valid + // on other systems bwd-slashes are valid // filename character, eg /f\oo/ba\r.txt if (platform.isWindows) { path = path.replace(/\\/g, URI._slash); diff --git a/src/vs/base/node/config.ts b/src/vs/base/node/config.ts index 3cd0c58c30c..43f176a397c 100644 --- a/src/vs/base/node/config.ts +++ b/src/vs/base/node/config.ts @@ -29,6 +29,7 @@ export interface IConfigOptions { defaultConfig?: T; changeBufferDelay?: number; parse?: (content: string, errors: any[]) => T; + initCallback?: (config: T) => void; } /** @@ -75,6 +76,9 @@ export class ConfigWatcher implements IConfigWatcher, IDisposable { if (!this.loaded) { this.updateCache(config); // prevent race condition if config was loaded sync already } + if (this.options.initCallback) { + this.options.initCallback(this.getConfig()); + } }); } diff --git a/src/vs/base/parts/tree/browser/treeView.ts b/src/vs/base/parts/tree/browser/treeView.ts index 115c07c566c..f1df2bb2261 100644 --- a/src/vs/base/parts/tree/browser/treeView.ts +++ b/src/vs/base/parts/tree/browser/treeView.ts @@ -524,7 +524,7 @@ export class TreeView extends HeightMap { } this.viewListeners.push(DOM.addDisposableListener(window, 'dragover', (e) => this.onDragOver(e))); - this.viewListeners.push(DOM.addDisposableListener(window, 'drop', (e) => this.onDrop(e))); + this.viewListeners.push(DOM.addDisposableListener(this.wrapper, 'drop', (e) => this.onDrop(e))); this.viewListeners.push(DOM.addDisposableListener(window, 'dragend', (e) => this.onDragEnd(e))); this.viewListeners.push(DOM.addDisposableListener(window, 'dragleave', (e) => this.onDragOver(e))); diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index d0f242435e3..3d015e909a2 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -44,13 +44,14 @@ import { IDisposable, dispose } from "vs/base/common/lifecycle"; import { ConfigurationService } from "vs/platform/configuration/node/configurationService"; import { TPromise } from "vs/base/common/winjs.base"; import { IWindowsMainService } from "vs/platform/windows/electron-main/windows"; -import { IHistoryMainService } from "vs/platform/history/electron-main/historyMainService"; +import { IHistoryMainService } from "vs/platform/history/common/history"; import { isUndefinedOrNull } from 'vs/base/common/types'; import { CodeWindow } from "vs/code/electron-main/window"; -import { isParent } from 'vs/platform/files/common/files'; -import { isEqual } from 'vs/base/common/paths'; import { KeyboardLayoutMonitor } from "vs/code/electron-main/keyboard"; import URI from 'vs/base/common/uri'; +import { WorkspacesChannel } from "vs/platform/workspaces/common/workspacesIpc"; +import { IWorkspacesMainService, IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier } from "vs/platform/workspaces/common/workspaces"; +import { findWindowOnWorkspaceOrFolder } from "vs/code/node/windowsFinder"; export class CodeApplication { private toDispose: IDisposable[]; @@ -95,9 +96,9 @@ export class CodeApplication { } } - console.error('[uncaught exception in main]: ' + err); + this.logService.error(`[uncaught exception in main]: ${err}`); if (err.stack) { - console.error(err.stack); + this.logService.error(err.stack); } }); @@ -136,12 +137,12 @@ export class CodeApplication { } // Otherwise prevent loading - console.error('Prevented webview attach'); + this.logService.error('webContents#web-contents-created: Prevented webview attach'); event.preventDefault(); }); contents.on('will-navigate', event => { - console.error('Prevented webcontent navigation'); + this.logService.error('webContents#will-navigate: Prevented webcontent navigation'); event.preventDefault(); }); }); @@ -198,28 +199,29 @@ export class CodeApplication { if (!webContents.isDestroyed()) { webContents.send('vscode:acceptShellEnv', {}); } - console.error('Error fetching shell env', err); + + this.logService.error('Error fetching shell env', err); }); }); - ipc.on('vscode:broadcast', (event, windowId: number, target: string, broadcast: { channel: string; payload: any; }) => { + ipc.on('vscode:broadcast', (event, windowId: number, target: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier, broadcast: { channel: string; payload: any; }) => { if (this.windowsMainService && broadcast.channel && !isUndefinedOrNull(broadcast.payload)) { this.logService.log('IPC#vscode:broadcast', target, broadcast.channel, broadcast.payload); // Handle specific events on main side this.onBroadcast(broadcast.channel, broadcast.payload); - // Send to windows + // Send to specific window if target is provided if (target) { - const otherWindowsWithTarget = this.windowsMainService.getWindows().filter(w => w.id !== windowId && typeof w.openedWorkspacePath === 'string'); - const directTargetMatch = otherWindowsWithTarget.filter(w => isEqual(target, w.openedWorkspacePath, !platform.isLinux /* ignorecase */)); - const parentTargetMatch = otherWindowsWithTarget.filter(w => isParent(target, w.openedWorkspacePath, !platform.isLinux /* ignorecase */)); - - const targetWindow = directTargetMatch.length ? directTargetMatch[0] : parentTargetMatch[0]; // prefer direct match over parent match + const otherWindowsWithTarget = this.windowsMainService.getWindows().filter(w => w.id !== windowId && (w.openedWorkspace || w.openedFolderPath)); + const targetWindow = findWindowOnWorkspaceOrFolder(otherWindowsWithTarget, target); if (targetWindow) { targetWindow.send('vscode:broadcast', broadcast); } - } else { + } + + // Otherwise send to all windows + else { this.windowsMainService.sendToAll('vscode:broadcast', broadcast, [windowId]); } } @@ -333,6 +335,10 @@ export class CodeApplication { const urlChannel = appInstantiationService.createInstance(URLChannel, urlService); this.electronIpcServer.registerChannel('url', urlChannel); + const workspacesService = accessor.get(IWorkspacesMainService); + const workspacesChannel = appInstantiationService.createInstance(WorkspacesChannel, workspacesService); + this.electronIpcServer.registerChannel('workspaces', workspacesChannel); + const windowsService = accessor.get(IWindowsService); const windowsChannel = new WindowsChannel(windowsService); this.electronIpcServer.registerChannel('windows', windowsChannel); @@ -376,7 +382,7 @@ export class CodeApplication { // Jump List this.historyService.updateWindowsJumpList(); - this.historyService.onRecentPathsChange(() => this.historyService.updateWindowsJumpList()); + this.historyService.onRecentlyOpenedChange(() => this.historyService.updateWindowsJumpList()); // Start shared process here this.sharedProcess.spawn(); diff --git a/src/vs/code/electron-main/main.ts b/src/vs/code/electron-main/main.ts index 8e520e45a6e..1db3cf08421 100644 --- a/src/vs/code/electron-main/main.ts +++ b/src/vs/code/electron-main/main.ts @@ -33,13 +33,17 @@ import { IURLService } from 'vs/platform/url/common/url'; import { URLService } from 'vs/platform/url/electron-main/urlService'; import * as fs from 'original-fs'; import { CodeApplication } from "vs/code/electron-main/app"; -import { HistoryMainService, IHistoryMainService } from "vs/platform/history/electron-main/historyMainService"; +import { HistoryMainService } from "vs/platform/history/electron-main/historyMainService"; +import { IHistoryMainService } from "vs/platform/history/common/history"; +import { WorkspacesMainService } from "vs/platform/workspaces/electron-main/workspacesMainService"; +import { IWorkspacesMainService } from "vs/platform/workspaces/common/workspaces"; function createServices(args: ParsedArgs): IInstantiationService { const services = new ServiceCollection(); services.set(IEnvironmentService, new SyncDescriptor(EnvironmentService, args, process.execPath)); services.set(ILogService, new SyncDescriptor(LogMainService)); + services.set(IWorkspacesMainService, new SyncDescriptor(WorkspacesMainService)); services.set(IHistoryMainService, new SyncDescriptor(HistoryMainService)); services.set(ILifecycleService, new SyncDescriptor(LifecycleService)); services.set(IStorageService, new SyncDescriptor(StorageService)); @@ -111,8 +115,9 @@ function setupIPC(accessor: ServicesAccessor): TPromise { // Tests from CLI require to be the only instance currently if (environmentService.extensionTestsPath && !environmentService.debugExtensionHost.break) { const msg = 'Running extension tests from the command line is currently only supported if no other instance of Code is running.'; - console.error(msg); + logService.error(msg); client.dispose(); + return TPromise.wrapError(new Error(msg)); } diff --git a/src/vs/code/electron-main/menus.ts b/src/vs/code/electron-main/menus.ts index 15ec1c62f18..c58e9040d27 100644 --- a/src/vs/code/electron-main/menus.ts +++ b/src/vs/code/electron-main/menus.ts @@ -21,7 +21,8 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { tildify } from 'vs/base/common/labels'; import { KeybindingsResolver } from "vs/code/electron-main/keyboard"; import { IWindowsMainService } from "vs/platform/windows/electron-main/windows"; -import { IHistoryMainService } from "vs/platform/history/electron-main/historyMainService"; +import { IHistoryMainService } from "vs/platform/history/common/history"; +import { IWorkspaceIdentifier, IWorkspacesMainService, getWorkspaceLabel, ISingleFolderWorkspaceIdentifier } from "vs/platform/workspaces/common/workspaces"; interface IExtensionViewlet { id: string; @@ -77,7 +78,8 @@ export class CodeMenu { @IWindowsMainService private windowsService: IWindowsMainService, @IEnvironmentService private environmentService: IEnvironmentService, @ITelemetryService private telemetryService: ITelemetryService, - @IHistoryMainService private historyService: IHistoryMainService + @IHistoryMainService private historyService: IHistoryMainService, + @IWorkspacesMainService private workspacesService: IWorkspacesMainService ) { this.extensionViewlets = []; @@ -100,7 +102,7 @@ export class CodeMenu { // Listen to some events from window service this.windowsService.onPathsOpen(paths => this.updateMenu()); - this.historyService.onRecentPathsChange(paths => this.updateMenu()); + this.historyService.onRecentlyOpenedChange(() => this.updateMenu()); this.windowsService.onWindowClose(_ => this.onClose(this.windowsService.getWindowCount())); // Listen to extension viewlets @@ -249,7 +251,6 @@ export class CodeMenu { const debugMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mDebug', comment: ['&& denotes a mnemonic'] }, "&&Debug")), submenu: debugMenu }); this.setDebugMenu(debugMenu); - // Mac: Window let macWindowMenuItem: Electron.MenuItem; if (isMacintosh) { @@ -354,7 +355,22 @@ export class CodeMenu { const openRecent = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'miOpenRecent', comment: ['&& denotes a mnemonic'] }, "Open &&Recent")), submenu: openRecentMenu, enabled: openRecentMenu.items.length > 0 }); const isMultiRootEnabled = (product.quality !== 'stable'); // TODO@Ben multi root + + const workspacesMenu = new Menu(); + const workspaces = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'miWorkspaces', comment: ['&& denotes a mnemonic'] }, "Workspaces")), submenu: workspacesMenu }); + + const newWorkspace = this.createMenuItem(nls.localize({ key: 'miNewWorkspace', comment: ['&& denotes a mnemonic'] }, "&&New Workspace..."), 'workbench.action.newWorkspace'); + const openWorkspace = this.createMenuItem(nls.localize({ key: 'miOpenWorkspace', comment: ['&& denotes a mnemonic'] }, "&&Open Workspace..."), 'workbench.action.openWorkspace'); + const saveWorkspace = this.createMenuItem(nls.localize({ key: 'miSaveWorkspace', comment: ['&& denotes a mnemonic'] }, "&&Save Workspace..."), 'workbench.action.saveWorkspace', this.windowsService.getWindowCount() > 0); const addFolder = this.createMenuItem(nls.localize({ key: 'miAddFolderToWorkspace', comment: ['&& denotes a mnemonic'] }, "&&Add Folder to Workspace..."), 'workbench.action.addRootFolder', this.windowsService.getWindowCount() > 0); + [ + newWorkspace, + openWorkspace, + __separator__(), + saveWorkspace, + __separator__(), + addFolder + ].forEach(item => workspacesMenu.append(item)); const saveFile = this.createMenuItem(nls.localize({ key: 'miSave', comment: ['&& denotes a mnemonic'] }, "&&Save"), 'workbench.action.files.save', this.windowsService.getWindowCount() > 0); const saveFileAs = this.createMenuItem(nls.localize({ key: 'miSaveAs', comment: ['&& denotes a mnemonic'] }, "Save &&As..."), 'workbench.action.files.saveAs', this.windowsService.getWindowCount() > 0); @@ -369,7 +385,7 @@ export class CodeMenu { const revertFile = this.createMenuItem(nls.localize({ key: 'miRevert', comment: ['&& denotes a mnemonic'] }, "Re&&vert File"), 'workbench.action.files.revert', this.windowsService.getWindowCount() > 0); const closeWindow = new MenuItem(this.likeAction('workbench.action.closeWindow', { label: this.mnemonicLabel(nls.localize({ key: 'miCloseWindow', comment: ['&& denotes a mnemonic'] }, "Clos&&e Window")), click: () => this.windowsService.getLastActiveWindow().win.close(), enabled: this.windowsService.getWindowCount() > 0 })); - const closeFolder = this.createMenuItem(nls.localize({ key: 'miCloseFolder', comment: ['&& denotes a mnemonic'] }, "Close &&Folder"), 'workbench.action.closeFolder'); + const closeWorkspace = this.createMenuItem(nls.localize({ key: 'miCloseWorkspace', comment: ['&& denotes a mnemonic'] }, "Close &&Workspace"), 'workbench.action.closeFolder'); const closeEditor = this.createMenuItem(nls.localize({ key: 'miCloseEditor', comment: ['&& denotes a mnemonic'] }, "&&Close Editor"), 'workbench.action.closeActiveEditor'); const exit = new MenuItem(this.likeAction('workbench.action.quit', { label: this.mnemonicLabel(nls.localize({ key: 'miExit', comment: ['&& denotes a mnemonic'] }, "E&&xit")), click: () => this.windowsService.quit() })); @@ -383,7 +399,7 @@ export class CodeMenu { !isMacintosh ? openFolder : null, openRecent, isMultiRootEnabled ? __separator__() : null, - isMultiRootEnabled ? addFolder : null, + isMultiRootEnabled ? workspaces : null, __separator__(), saveFile, saveFileAs, @@ -395,7 +411,7 @@ export class CodeMenu { !isMacintosh ? __separator__() : null, revertFile, closeEditor, - closeFolder, + closeWorkspace, closeWindow, !isMacintosh ? __separator__() : null, !isMacintosh ? exit : null @@ -427,14 +443,14 @@ export class CodeMenu { private setOpenRecentMenu(openRecentMenu: Electron.Menu): void { openRecentMenu.append(this.createMenuItem(nls.localize({ key: 'miReopenClosedEditor', comment: ['&& denotes a mnemonic'] }, "&&Reopen Closed Editor"), 'workbench.action.reopenClosedEditor')); - const { folders, files } = this.historyService.getRecentPathsList(); + const { workspaces, files } = this.historyService.getRecentlyOpened(); - // Folders - if (folders.length > 0) { + // Workspaces + if (workspaces.length > 0) { openRecentMenu.append(__separator__()); - for (let i = 0; i < CodeMenu.MAX_MENU_RECENT_ENTRIES && i < folders.length; i++) { - openRecentMenu.append(this.createOpenRecentMenuItem(folders[i], 'openRecentFolder')); + for (let i = 0; i < CodeMenu.MAX_MENU_RECENT_ENTRIES && i < workspaces.length; i++) { + openRecentMenu.append(this.createOpenRecentMenuItem(workspaces[i], 'openRecentWorkspace')); } } @@ -447,21 +463,25 @@ export class CodeMenu { } } - if (folders.length || files.length) { + if (workspaces.length || files.length) { openRecentMenu.append(__separator__()); openRecentMenu.append(this.createMenuItem(nls.localize({ key: 'miMore', comment: ['&& denotes a mnemonic'] }, "&&More..."), 'workbench.action.openRecent')); openRecentMenu.append(__separator__()); - openRecentMenu.append(this.createMenuItem(nls.localize({ key: 'miClearRecentOpen', comment: ['&& denotes a mnemonic'] }, "&&Clear Recent Files"), 'workbench.action.clearRecentFiles')); + openRecentMenu.append(this.createMenuItem(nls.localize({ key: 'miClearRecentOpen', comment: ['&& denotes a mnemonic'] }, "&&Clear Recently Opened"), 'workbench.action.clearRecentFiles')); } } - private createOpenRecentMenuItem(path: string, commandId: string): Electron.MenuItem { + private createOpenRecentMenuItem(recent: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier, commandId: string): Electron.MenuItem { + const label = (typeof recent === 'string') ? this.unmnemonicLabel(tildify(recent, this.environmentService.userHome)) : getWorkspaceLabel(this.environmentService, recent); + const path = (typeof recent === 'string') ? recent : recent.configPath; + return new MenuItem(this.likeAction(commandId, { - label: this.unmnemonicLabel(tildify(path, this.environmentService.userHome)), click: (menuItem, win, event) => { + label, + click: (menuItem, win, event) => { const openInNewWindow = this.isOptionClick(event); const success = this.windowsService.open({ context: OpenContext.MENU, cli: this.environmentService.args, pathsToOpen: [path], forceNewWindow: openInNewWindow }).length > 0; if (!success) { - this.historyService.removeFromRecentPathsList(path); + this.historyService.removeFromRecentlyOpened(recent); } } }, false)); @@ -803,7 +823,6 @@ export class CodeMenu { __separator__(), installAdditionalDebuggers ].forEach(item => debugMenu.append(item)); - } private setMacWindowMenu(macWindowMenu: Electron.Menu): void { diff --git a/src/vs/code/electron-main/window.ts b/src/vs/code/electron-main/window.ts index 7b2796150c0..433f2ac9382 100644 --- a/src/vs/code/electron-main/window.ts +++ b/src/vs/code/electron-main/window.ts @@ -9,6 +9,7 @@ import * as path from 'path'; import * as objects from 'vs/base/common/objects'; import { stopProfiling } from 'vs/base/node/profiler'; import nls = require('vs/nls'); +import URI from "vs/base/common/uri"; import { IStorageService } from 'vs/platform/storage/node/storage'; import { shell, screen, BrowserWindow, systemPreferences, app } from 'electron'; import { TPromise, TValueCallback } from 'vs/base/common/winjs.base'; @@ -23,6 +24,7 @@ import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { KeyboardLayoutMonitor } from 'vs/code/electron-main/keyboard'; import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; import { ICodeWindow } from "vs/platform/windows/electron-main/windows"; +import { IWorkspaceIdentifier } from "vs/platform/workspaces/common/workspaces"; export interface IWindowState { width?: number; @@ -110,10 +112,6 @@ export class CodeWindow implements ICodeWindow { // respect configured menu bar visibility this.onConfigurationUpdated(); - // TODO@joao: hook this up to some initialization routine this causes a race between setting the headers and doing - // a request that needs them. chances are low - this.setCommonHTTPHeaders(); - // Eventing this.registerListeners(); } @@ -203,20 +201,6 @@ export class CodeWindow implements ICodeWindow { this._lastFocusTime = Date.now(); // since we show directly, we need to set the last focus time too } - private setCommonHTTPHeaders(): void { - getCommonHTTPHeaders().done(headers => { - if (!this._win) { - return; - } - - const urls = ['https://marketplace.visualstudio.com/*', 'https://*.vsassets.io/*']; - - this._win.webContents.session.webRequest.onBeforeSendHeaders({ urls }, (details, cb) => { - cb({ cancel: false, requestHeaders: objects.assign(details.requestHeaders, headers) }); - }); - }); - } - public hasHiddenTitleBarStyle(): boolean { return this.hiddenTitleBarStyle; } @@ -277,14 +261,18 @@ export class CodeWindow implements ICodeWindow { return this._lastFocusTime; } - public get openedWorkspacePath(): string { - return this.currentConfig ? this.currentConfig.workspacePath : void 0; - } - public get backupPath(): string { return this.currentConfig ? this.currentConfig.backupPath : void 0; } + public get openedWorkspace(): IWorkspaceIdentifier { + return this.currentConfig ? this.currentConfig.workspace : void 0; + } + + public get openedFolderPath(): string { + return this.currentConfig ? this.currentConfig.folderPath : void 0; + } + public get openedFilePath(): string { return this.currentConfig && this.currentConfig.filesToOpen && this.currentConfig.filesToOpen[0] && this.currentConfig.filesToOpen[0].filePath; } @@ -315,6 +303,42 @@ export class CodeWindow implements ICodeWindow { private registerListeners(): void { + // Set common HTTP headers + // TODO@joao: hook this up to some initialization routine this causes a race between setting the headers and doing + // a request that needs them. chances are low + getCommonHTTPHeaders().done(headers => { + if (!this._win) { + return; + } + + const urls = ['https://marketplace.visualstudio.com/*', 'https://*.vsassets.io/*']; + + this._win.webContents.session.webRequest.onBeforeSendHeaders({ urls }, (details, cb) => { + cb({ cancel: false, requestHeaders: objects.assign(details.requestHeaders, headers) }); + }); + }); + + // Prevent loading of svgs + this._win.webContents.session.webRequest.onBeforeRequest((details, callback) => { + if (details.url.indexOf('.svg') > 0) { + const uri = URI.parse(details.url); + if (uri && !uri.scheme.match(/file/i) && (uri.path as any).endsWith('.svg')) { + return callback({ cancel: true }); + } + } + + return callback({}); + }); + + this._win.webContents.session.webRequest.onHeadersReceived((details, callback) => { + const contentType: string[] = (details.responseHeaders['content-type'] || details.responseHeaders['Content-Type']) as any; + if (contentType && Array.isArray(contentType) && contentType.some(x => x.toLowerCase().indexOf('image/svg') >= 0)) { + return callback({ cancel: true }); + } + + return callback({ cancel: false, responseHeaders: details.responseHeaders }); + }); + // Remember that we loaded this._win.webContents.on('did-finish-load', () => { this._readyState = ReadyState.LOADING; @@ -364,7 +388,7 @@ export class CodeWindow implements ICodeWindow { // Window Failed to load this._win.webContents.on('did-fail-load', (event: Event, errorCode: string, errorDescription: string) => { - console.warn('[electron event]: fail to load, ', errorDescription); + this.logService.warn('[electron event]: fail to load, ', errorDescription); }); // Prevent any kind of navigation triggered by the user! @@ -453,7 +477,7 @@ export class CodeWindow implements ICodeWindow { // (--prof-startup) save profile to disk const { profileStartup } = this.environmentService; if (profileStartup) { - stopProfiling(profileStartup.dir, profileStartup.prefix).done(undefined, err => console.error(err)); + stopProfiling(profileStartup.dir, profileStartup.prefix).done(undefined, err => this.logService.error(err)); } } diff --git a/src/vs/code/electron-main/windows.ts b/src/vs/code/electron-main/windows.ts index 2b62b94920d..ad329e47ecd 100644 --- a/src/vs/code/electron-main/windows.ts +++ b/src/vs/code/electron-main/windows.ts @@ -10,7 +10,6 @@ import * as fs from 'original-fs'; import * as nls from 'vs/nls'; import * as arrays from 'vs/base/common/arrays'; import { assign, mixin } from 'vs/base/common/objects'; -import URI from 'vs/base/common/uri'; import { IBackupMainService } from 'vs/platform/backup/common/backup'; import { IEnvironmentService, ParsedArgs } from 'vs/platform/environment/common/environment'; import { IStorageService } from 'vs/platform/storage/node/storage'; @@ -21,16 +20,16 @@ import { ILifecycleService, UnloadReason } from 'vs/platform/lifecycle/electron- import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ILogService } from 'vs/platform/log/common/log'; import { IWindowSettings, OpenContext, IPath, IWindowConfiguration } from 'vs/platform/windows/common/windows'; -import { getLastActiveWindow, findBestWindowOrFolderForFile, findWindowOnWorkspace } from 'vs/code/node/windowsFinder'; +import { getLastActiveWindow, findBestWindowOrFolderForFile, findWindowOnFolder, findWindowOnWorkspace } from 'vs/code/node/windowsFinder'; import CommonEvent, { Emitter } from 'vs/base/common/event'; import product from 'vs/platform/node/product'; import { ITelemetryService, ITelemetryData } from 'vs/platform/telemetry/common/telemetry'; import { isEqual } from 'vs/base/common/paths'; import { IWindowsMainService, IOpenConfiguration } from "vs/platform/windows/electron-main/windows"; -import { IHistoryMainService } from "vs/platform/history/electron-main/historyMainService"; +import { IHistoryMainService } from "vs/platform/history/common/history"; import { IProcessEnvironment, isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; import { TPromise } from "vs/base/common/winjs.base"; - +import { IWorkspacesMainService, IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier } from "vs/platform/workspaces/common/workspaces"; enum WindowError { UNRESPONSIVE, @@ -41,17 +40,25 @@ interface INewWindowState extends ISingleWindowState { hasDefaultState?: boolean; } -interface IWindowState { +interface ILegacyWindowState extends IWindowState { workspacePath?: string; +} + +interface IWindowState { + workspace?: IWorkspaceIdentifier; + folderPath?: string; backupPath: string; uiState: ISingleWindowState; } +interface ILegacyWindowsState extends IWindowsState { + openedFolders?: IWindowState[]; +} + interface IWindowsState { lastActiveWindow?: IWindowState; lastPluginDevelopmentHostWindow?: IWindowState; openedWindows: IWindowState[]; - openedFolders?: IWindowState[]; // TODO@Ben deprecated } type RestoreWindowsSetting = 'all' | 'folders' | 'one' | 'none'; @@ -59,7 +66,9 @@ type RestoreWindowsSetting = 'all' | 'folders' | 'one' | 'none'; interface IOpenBrowserWindowOptions { userEnv?: IProcessEnvironment; cli?: ParsedArgs; - workspacePath?: string; + + workspace?: IWorkspaceIdentifier; + folderPath?: string; initialStartup?: boolean; @@ -70,13 +79,16 @@ interface IOpenBrowserWindowOptions { forceNewWindow?: boolean; windowToUse?: CodeWindow; - emptyWorkspaceBackupFolder?: string; + emptyWindowBackupFolder?: string; } interface IWindowToOpen extends IPath { - // the workspace spath for a Code instance to open - workspacePath?: string; + // the workspace for a Code instance to open + workspace?: IWorkspaceIdentifier; + + // the folder path for a Code instance to open + folderPath?: string; // the backup spath for a Code instance to use backupPath?: string; @@ -120,19 +132,37 @@ export class WindowsManager implements IWindowsMainService { @IBackupMainService private backupService: IBackupMainService, @ITelemetryService private telemetryService: ITelemetryService, @IConfigurationService private configurationService: IConfigurationService, - @IHistoryMainService private historyService: IHistoryMainService + @IHistoryMainService private historyService: IHistoryMainService, + @IWorkspacesMainService private workspacesService: IWorkspacesMainService ) { this.windowsState = this.storageService.getItem(WindowsManager.windowsStateStorageKey) || { openedWindows: [] }; + this.fileDialog = new FileDialog(environmentService, telemetryService, storageService, this); + + this.migrateLegacyWindowState(); + } + + private migrateLegacyWindowState(): void { + const state: ILegacyWindowsState = this.windowsState; // TODO@Ben migration from previous openedFolders to new openedWindows property - if (Array.isArray(this.windowsState.openedFolders) && this.windowsState.openedFolders.length > 0) { - this.windowsState.openedWindows = this.windowsState.openedFolders; - this.windowsState.openedFolders = void 0; - } else if (!this.windowsState.openedWindows) { - this.windowsState.openedWindows = []; + if (Array.isArray(state.openedFolders) && state.openedFolders.length > 0) { + state.openedWindows = state.openedFolders; + state.openedFolders = void 0; + } else if (!state.openedWindows) { + state.openedWindows = []; } - this.fileDialog = new FileDialog(environmentService, telemetryService, storageService, this); + // TODO@Ben migration from previous workspacePath in window state to folderPath + const states: ILegacyWindowState[] = []; + states.push(state.lastActiveWindow); + states.push(state.lastPluginDevelopmentHostWindow); + states.push(...state.openedWindows); + states.forEach(state => { + if (state && typeof state.workspacePath === 'string') { + state.folderPath = state.workspacePath; + state.workspacePath = void 0; + } + }); } public ready(initialUserEnv: IProcessEnvironment): void { @@ -180,7 +210,7 @@ export class WindowsManager implements IWindowsMainService { // and then onBeforeQuit(). Using the quit action however will first issue onBeforeQuit() // and then onBeforeWindowClose(). private onBeforeQuit(): void { - const currentWindowsState: IWindowsState = { + const currentWindowsState: ILegacyWindowsState = { openedWindows: [], openedFolders: [], // TODO@Ben migration so that old clients do not fail over data (prevents NPEs) lastPluginDevelopmentHostWindow: this.windowsState.lastPluginDevelopmentHostWindow, @@ -195,14 +225,14 @@ export class WindowsManager implements IWindowsMainService { } if (activeWindow) { - currentWindowsState.lastActiveWindow = { workspacePath: activeWindow.openedWorkspacePath, uiState: activeWindow.serializeWindowState(), backupPath: activeWindow.backupPath }; + currentWindowsState.lastActiveWindow = this.toWindowState(activeWindow); } } // 2.) Find extension host window const extensionHostWindow = WindowsManager.WINDOWS.filter(w => w.isExtensionDevelopmentHost && !w.isExtensionTestHost)[0]; if (extensionHostWindow) { - currentWindowsState.lastPluginDevelopmentHostWindow = { workspacePath: extensionHostWindow.openedWorkspacePath, uiState: extensionHostWindow.serializeWindowState(), backupPath: extensionHostWindow.backupPath }; + currentWindowsState.lastPluginDevelopmentHostWindow = this.toWindowState(extensionHostWindow); } // 3.) All windows (except extension host) for N >= 2 to support restoreWindows: all or for auto update @@ -211,13 +241,7 @@ export class WindowsManager implements IWindowsMainService { // so if we ever want to persist the UI state of the last closed window (window count === 1), it has // to come from the stored lastClosedWindowState on Win/Linux at least if (this.getWindowCount() > 1) { - currentWindowsState.openedWindows = WindowsManager.WINDOWS.filter(w => !w.isExtensionDevelopmentHost).map(w => { - return { - workspacePath: w.openedWorkspacePath, - uiState: w.serializeWindowState(), - backupPath: w.backupPath - }; - }); + currentWindowsState.openedWindows = WindowsManager.WINDOWS.filter(w => !w.isExtensionDevelopmentHost).map(w => this.toWindowState(w)); } // Persist @@ -231,15 +255,18 @@ export class WindowsManager implements IWindowsMainService { } // On Window close, update our stored UI state of this window - const state: IWindowState = { workspacePath: win.openedWorkspacePath, uiState: win.serializeWindowState(), backupPath: win.backupPath }; + const state: IWindowState = this.toWindowState(win); if (win.isExtensionDevelopmentHost && !win.isExtensionTestHost) { this.windowsState.lastPluginDevelopmentHostWindow = state; // do not let test run window state overwrite our extension development state } - // Any non extension host window with same workspace - else if (!win.isExtensionDevelopmentHost && !!win.openedWorkspacePath) { + // Any non extension host window with same workspace or folder + else if (!win.isExtensionDevelopmentHost && (!!win.openedWorkspace || !!win.openedFolderPath)) { this.windowsState.openedWindows.forEach(o => { - if (isEqual(o.workspacePath, win.openedWorkspacePath, !isLinux /* ignorecase */)) { + const sameWorkspace = win.openedWorkspace && o.workspace.id === win.openedWorkspace.id; + const sameFolder = win.openedFolderPath && isEqual(o.folderPath, win.openedFolderPath, !isLinux /* ignorecase */); + + if (sameWorkspace || sameFolder) { o.uiState = state.uiState; } }); @@ -254,15 +281,25 @@ export class WindowsManager implements IWindowsMainService { } } + private toWindowState(win: CodeWindow): IWindowState { + return { + workspace: win.openedWorkspace, + folderPath: win.openedFolderPath, + backupPath: win.backupPath, + uiState: win.serializeWindowState() + }; + } + + public closeWorkspace(win: CodeWindow): void { + this.openInBrowserWindow({ + cli: this.environmentService.args, + windowToUse: win + }); + } + public open(openConfig: IOpenConfiguration): CodeWindow[] { const windowsToOpen = this.getWindowsToOpen(openConfig); - // - // These are windows to open to show either folders or files (including diffing files or creating them) - // - const foldersToOpen = arrays.distinct(windowsToOpen.filter(win => win.workspacePath && !win.filePath).map(win => win.workspacePath), folder => isLinux ? folder : folder.toLowerCase()); // prevent duplicates - const emptyToOpen = windowsToOpen.filter(win => !win.workspacePath && !win.filePath && !win.backupPath).length; - let filesToOpen = windowsToOpen.filter(path => !!path.filePath && !path.createFilePath); let filesToCreate = windowsToOpen.filter(path => !!path.filePath && path.createFilePath); let filesToDiff: IPath[]; @@ -275,16 +312,32 @@ export class WindowsManager implements IWindowsMainService { } // - // These are windows to restore because of hot-exit + // These are windows to open to show workspaces + // + const workspacesToOpen = arrays.distinct(windowsToOpen.filter(win => !!win.workspace).map(win => win.workspace), workspace => workspace.id); // prevent duplicates + + // + // These are windows to open to show either folders or files (including diffing files or creating them) + // + const foldersToOpen = arrays.distinct(windowsToOpen.filter(win => win.folderPath && !win.filePath).map(win => win.folderPath), folder => isLinux ? folder : folder.toLowerCase()); // prevent duplicates + + // + // These are windows to restore because of hot-exit or empty windows from previous session // const hotExitRestore = (openConfig.initialStartup && !openConfig.cli.extensionDevelopmentPath); - const foldersToRestore = hotExitRestore ? this.backupService.getWorkspaceBackupPaths() : []; - let emptyToRestore = hotExitRestore ? this.backupService.getEmptyWorkspaceBackupPaths() : []; - emptyToRestore.push(...windowsToOpen.filter(w => !w.workspacePath && w.backupPath).map(w => path.basename(w.backupPath))); // add empty workspaces with backupPath + const foldersToRestore = hotExitRestore ? this.backupService.getFolderBackupPaths() : []; + const workspacesToRestore = hotExitRestore ? this.backupService.getWorkspaceBackups() : []; + let emptyToRestore = hotExitRestore ? this.backupService.getEmptyWindowBackupPaths() : []; + emptyToRestore.push(...windowsToOpen.filter(w => !w.workspace && !w.folderPath && w.backupPath).map(w => path.basename(w.backupPath))); // add empty windows with backupPath emptyToRestore = arrays.distinct(emptyToRestore); // prevent duplicates + // + // These are empty windows to open + // + const emptyToOpen = windowsToOpen.filter(win => !win.workspace && !win.folderPath && !win.filePath && !win.backupPath).length; + // Open based on config - const usedWindows = this.doOpen(openConfig, foldersToOpen, foldersToRestore, emptyToRestore, emptyToOpen, filesToOpen, filesToCreate, filesToDiff); + const usedWindows = this.doOpen(openConfig, workspacesToOpen, workspacesToRestore, foldersToOpen, foldersToRestore, emptyToRestore, emptyToOpen, filesToOpen, filesToCreate, filesToDiff); // Make sure the last active window gets focus if we opened multiple if (usedWindows.length > 1 && this.windowsState.lastActiveWindow) { @@ -297,17 +350,18 @@ export class WindowsManager implements IWindowsMainService { // Remember in recent document list (unless this opens for extension development) // Also do not add paths when files are opened for diffing, only if opened individually if (!usedWindows.some(w => w.isExtensionDevelopmentHost) && !openConfig.cli.diff) { - const recentPaths: { path: string; isFile?: boolean; }[] = []; + const recentlyOpenedWorkspaces: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier)[] = []; + const recentlyOpenedFiles: string[] = []; windowsToOpen.forEach(win => { - if (win.filePath || win.workspacePath) { - recentPaths.push({ path: win.filePath || win.workspacePath, isFile: !!win.filePath }); + if (win.workspace || win.folderPath) { + recentlyOpenedWorkspaces.push(win.workspace || win.folderPath); + } else if (win.filePath) { + recentlyOpenedFiles.push(win.filePath); } }); - if (recentPaths.length) { - this.historyService.addToRecentPathsList(recentPaths); - } + this.historyService.addRecentlyOpened(recentlyOpenedWorkspaces, recentlyOpenedFiles); } // Emit events @@ -327,6 +381,8 @@ export class WindowsManager implements IWindowsMainService { private doOpen( openConfig: IOpenConfiguration, + workspacesToOpen: IWorkspaceIdentifier[], + workspacesToRestore: IWorkspaceIdentifier[], foldersToOpen: string[], foldersToRestore: string[], emptyToRestore: string[], @@ -343,7 +399,7 @@ export class WindowsManager implements IWindowsMainService { const usedWindows: CodeWindow[] = []; if (!foldersToOpen.length && !foldersToRestore.length && !emptyToRestore.length && (filesToOpen.length > 0 || filesToCreate.length > 0 || filesToDiff.length > 0)) { - // Open Files in last instance if any and flag tells us so + // Find suitable window or folder path to open files in const fileToCheck = filesToOpen[0] || filesToCreate[0] || filesToDiff[0]; const bestWindowOrFolder = findBestWindowOrFolderForFile({ windows: WindowsManager.WINDOWS, @@ -354,59 +410,102 @@ export class WindowsManager implements IWindowsMainService { userHome: this.environmentService.userHome }); - // We found a suitable window to open the files within - if (bestWindowOrFolder instanceof CodeWindow) { - const files = { filesToOpen, filesToCreate, filesToDiff }; // copy to object because they get reset shortly after - - const windowToUse = bestWindowOrFolder; - windowToUse.focus(); - windowToUse.ready().then(readyWindow => { - readyWindow.send('vscode:openFiles', files); - }); - - usedWindows.push(windowToUse); + // We found a suitable window to open the files within: send the files to open over + if (bestWindowOrFolder instanceof CodeWindow && bestWindowOrFolder.openedFolderPath) { + foldersToOpen.push(bestWindowOrFolder.openedFolderPath); } - // Otherwise open a new window with the best folder to use for the file + // We found a suitable folder to open: add it to foldersToOpen + else if (typeof bestWindowOrFolder === 'string') { + foldersToOpen.push(bestWindowOrFolder); + } + + // Finally, if no window or folder is found, just open the files in an empty window else { - const folderToOpen = bestWindowOrFolder; - const browserWindow = this.openInBrowserWindow({ + usedWindows.push(this.openInBrowserWindow({ userEnv: openConfig.userEnv, cli: openConfig.cli, initialStartup: openConfig.initialStartup, - workspacePath: folderToOpen, filesToOpen, filesToCreate, filesToDiff, forceNewWindow: true - }); - usedWindows.push(browserWindow); + })); + + // Reset these because we handled them + filesToOpen = []; + filesToCreate = []; + filesToDiff = []; + } + } + + // Handle workspaces to open (instructed and to restore) + const allWorkspacesToOpen = arrays.distinct([...workspacesToOpen, ...workspacesToRestore], workspace => workspace.id); // prevent duplicates + if (allWorkspacesToOpen.length > 0) { + + // Check for existing instances that have same workspace ID but different configuration path + // For now we reload that window with the new configuration so that the configuration path change + // can travel properly. + // TODO@Ben multi root revisit this once we can better transition between workspaces of the same id + allWorkspacesToOpen.forEach(workspaceToOpen => { + const existingWindow = findWindowOnWorkspace(WindowsManager.WINDOWS, workspaceToOpen); + if (existingWindow && existingWindow.openedWorkspace.configPath !== workspaceToOpen.configPath) { + usedWindows.push(this.doOpenFolderOrWorkspace(openConfig, { workspace: workspaceToOpen }, false, filesToOpen, filesToCreate, filesToDiff, existingWindow)); + + // Reset these because we handled them + filesToOpen = []; + filesToCreate = []; + filesToDiff = []; + + openFolderInNewWindow = true; // any other folders to open must open in new window then + } + }); + + // Check for existing instances + const windowsOnWorkspace = arrays.coalesce(allWorkspacesToOpen.map(workspaceToOpen => findWindowOnWorkspace(WindowsManager.WINDOWS, workspaceToOpen))); + if (windowsOnWorkspace.length > 0) { + const windowOnWorkspace = windowsOnWorkspace[0]; + + // Do open files + usedWindows.push(this.doOpenFilesInExistingWindow(windowOnWorkspace, filesToOpen, filesToCreate, filesToDiff)); + + // Reset these because we handled them + filesToOpen = []; + filesToCreate = []; + filesToDiff = []; openFolderInNewWindow = true; // any other folders to open must open in new window then } - // Reset these because we handled them - filesToOpen = []; - filesToCreate = []; - filesToDiff = []; + // Open remaining ones + allWorkspacesToOpen.forEach(workspaceToOpen => { + if (windowsOnWorkspace.some(win => win.openedWorkspace.id === workspaceToOpen.id)) { + return; // ignore folders that are already open + } + + // Do open folder + usedWindows.push(this.doOpenFolderOrWorkspace(openConfig, { workspace: workspaceToOpen }, openFolderInNewWindow, filesToOpen, filesToCreate, filesToDiff)); + + // Reset these because we handled them + filesToOpen = []; + filesToCreate = []; + filesToDiff = []; + + openFolderInNewWindow = true; // any other folders to open must open in new window then + }); } // Handle folders to open (instructed and to restore) - let allFoldersToOpen = arrays.distinct([...foldersToOpen, ...foldersToRestore], folder => isLinux ? folder : folder.toLowerCase()); // prevent duplicates + const allFoldersToOpen = arrays.distinct([...foldersToOpen, ...foldersToRestore], folder => isLinux ? folder : folder.toLowerCase()); // prevent duplicates if (allFoldersToOpen.length > 0) { // Check for existing instances - const windowsOnWorkspacePath = arrays.coalesce(allFoldersToOpen.map(folderToOpen => findWindowOnWorkspace(WindowsManager.WINDOWS, folderToOpen))); - if (windowsOnWorkspacePath.length > 0) { - const browserWindow = windowsOnWorkspacePath[0]; - browserWindow.focus(); // just focus one of them + const windowsOnFolderPath = arrays.coalesce(allFoldersToOpen.map(folderToOpen => findWindowOnFolder(WindowsManager.WINDOWS, folderToOpen))); + if (windowsOnFolderPath.length > 0) { + const windowOnFolderPath = windowsOnFolderPath[0]; - const files = { filesToOpen, filesToCreate, filesToDiff }; // copy to object because they get reset shortly after - browserWindow.ready().then(readyWindow => { - readyWindow.send('vscode:openFiles', files); - }); - - usedWindows.push(browserWindow); + // Do open files + usedWindows.push(this.doOpenFilesInExistingWindow(windowOnFolderPath, filesToOpen, filesToCreate, filesToDiff)); // Reset these because we handled them filesToOpen = []; @@ -418,22 +517,12 @@ export class WindowsManager implements IWindowsMainService { // Open remaining ones allFoldersToOpen.forEach(folderToOpen => { - if (windowsOnWorkspacePath.some(win => isEqual(win.openedWorkspacePath, folderToOpen, !isLinux /* ignorecase */))) { + if (windowsOnFolderPath.some(win => isEqual(win.openedFolderPath, folderToOpen, !isLinux /* ignorecase */))) { return; // ignore folders that are already open } - const browserWindow = this.openInBrowserWindow({ - userEnv: openConfig.userEnv, - cli: openConfig.cli, - initialStartup: openConfig.initialStartup, - workspacePath: folderToOpen, - filesToOpen, - filesToCreate, - filesToDiff, - forceNewWindow: openFolderInNewWindow, - windowToUse: openFolderInNewWindow ? void 0 : openConfig.windowToUse as CodeWindow - }); - usedWindows.push(browserWindow); + // Do open folder + usedWindows.push(this.doOpenFolderOrWorkspace(openConfig, { folderPath: folderToOpen }, openFolderInNewWindow, filesToOpen, filesToCreate, filesToDiff)); // Reset these because we handled them filesToOpen = []; @@ -444,10 +533,10 @@ export class WindowsManager implements IWindowsMainService { }); } - // Handle empty + // Handle empty to restore if (emptyToRestore.length > 0) { - emptyToRestore.forEach(emptyWorkspaceBackupFolder => { - const browserWindow = this.openInBrowserWindow({ + emptyToRestore.forEach(emptyWindowBackupFolder => { + usedWindows.push(this.openInBrowserWindow({ userEnv: openConfig.userEnv, cli: openConfig.cli, initialStartup: openConfig.initialStartup, @@ -455,9 +544,8 @@ export class WindowsManager implements IWindowsMainService { filesToCreate, filesToDiff, forceNewWindow: true, - emptyWorkspaceBackupFolder - }); - usedWindows.push(browserWindow); + emptyWindowBackupFolder + })); // Reset these because we handled them filesToOpen = []; @@ -468,17 +556,15 @@ export class WindowsManager implements IWindowsMainService { }); } - // Only open empty if no empty workspaces were restored + // Only open empty if no empty windows were restored else if (emptyToOpen > 0) { for (let i = 0; i < emptyToOpen; i++) { - const browserWindow = this.openInBrowserWindow({ + usedWindows.push(this.openInBrowserWindow({ userEnv: openConfig.userEnv, cli: openConfig.cli, initialStartup: openConfig.initialStartup, - forceNewWindow: openFolderInNewWindow, - windowToUse: openFolderInNewWindow ? void 0 : openConfig.windowToUse as CodeWindow - }); - usedWindows.push(browserWindow); + forceNewWindow: openFolderInNewWindow + })); openFolderInNewWindow = true; // any other folders to open must open in new window then } @@ -487,6 +573,33 @@ export class WindowsManager implements IWindowsMainService { return arrays.distinct(usedWindows); } + private doOpenFilesInExistingWindow(window: CodeWindow, filesToOpen: IPath[], filesToCreate: IPath[], filesToDiff: IPath[]): CodeWindow { + window.focus(); // make sure window has focus + + window.ready().then(readyWindow => { + readyWindow.send('vscode:openFiles', { filesToOpen, filesToCreate, filesToDiff }); + }); + + return window; + } + + private doOpenFolderOrWorkspace(openConfig: IOpenConfiguration, folderOrWorkspace: IWindowToOpen, openInNewWindow: boolean, filesToOpen: IPath[], filesToCreate: IPath[], filesToDiff: IPath[], windowToUse?: CodeWindow): CodeWindow { + const browserWindow = this.openInBrowserWindow({ + userEnv: openConfig.userEnv, + cli: openConfig.cli, + initialStartup: openConfig.initialStartup, + workspace: folderOrWorkspace.workspace, + folderPath: folderOrWorkspace.folderPath, + filesToOpen, + filesToCreate, + filesToDiff, + forceNewWindow: openInNewWindow, + windowToUse + }); + + return browserWindow; + } + private getWindowsToOpen(openConfig: IOpenConfiguration): IWindowToOpen[] { let windowsToOpen: IWindowToOpen[]; @@ -561,24 +674,28 @@ export class WindowsManager implements IWindowsMainService { switch (restoreWindows) { - // none: we always open an empty workspace + // none: we always open an empty window case 'none': return [Object.create(null)]; - // one: restore last opened folder or empty workspace + // one: restore last opened workspace/folder or empty window case 'one': if (lastActiveWindow) { - // return folder path if it is valid - const folder = lastActiveWindow.workspacePath; - if (folder) { - const validatedFolderPath = this.parsePath(folder); - if (validatedFolderPath) { - return [validatedFolderPath]; + // workspace + if (lastActiveWindow.workspace) { + return [{ workspace: lastActiveWindow.workspace }]; + } + + // folder (if path is valid) + else if (lastActiveWindow.folderPath) { + const validatedFolder = this.parsePath(lastActiveWindow.folderPath); + if (validatedFolder) { + return [validatedFolder]; } } - // otherwise use backup path to restore empty workspaces + // otherwise use backup path to restore empty windows else if (lastActiveWindow.backupPath) { return [{ backupPath: lastActiveWindow.backupPath }]; } @@ -589,20 +706,26 @@ export class WindowsManager implements IWindowsMainService { // folders: restore last opened folders only case 'all': case 'folders': + const windowsToOpen: IWindowToOpen[] = []; - // Windows with Folders - const lastOpenedFolders = this.windowsState.openedWindows.filter(w => !!w.workspacePath).map(o => o.workspacePath); - const lastActiveFolder = lastActiveWindow && lastActiveWindow.workspacePath; - if (lastActiveFolder) { - lastOpenedFolders.push(lastActiveFolder); + // Workspaces + const workspaces = this.windowsState.openedWindows.filter(w => !!w.workspace).map(w => w.workspace); + if (lastActiveWindow && lastActiveWindow.workspace) { + workspaces.push(lastActiveWindow.workspace); } + windowsToOpen.push(...workspaces.map(workspace => ({ workspace }))); - const windowsToOpen = lastOpenedFolders.map(candidate => this.parsePath(candidate)).filter(path => !!path); + // Folders + const folders = this.windowsState.openedWindows.filter(w => !!w.folderPath).map(w => w.folderPath); + if (lastActiveWindow && lastActiveWindow.folderPath) { + folders.push(lastActiveWindow.folderPath); + } + windowsToOpen.push(...folders.map(candidate => this.parsePath(candidate)).filter(path => !!path)); // Windows that were Empty if (restoreWindows === 'all') { - const lastOpenedEmpty = this.windowsState.openedWindows.filter(w => !w.workspacePath && w.backupPath).map(w => w.backupPath); - const lastActiveEmpty = lastActiveWindow && !lastActiveWindow.workspacePath && lastActiveWindow.backupPath; + const lastOpenedEmpty = this.windowsState.openedWindows.filter(w => !w.workspace && !w.folderPath && w.backupPath).map(w => w.backupPath); + const lastActiveEmpty = lastActiveWindow && !lastActiveWindow.workspace && !lastActiveWindow.folderPath && lastActiveWindow.backupPath; if (lastActiveEmpty) { lastOpenedEmpty.push(lastActiveEmpty); } @@ -617,7 +740,7 @@ export class WindowsManager implements IWindowsMainService { break; } - // Always fallback to empty workspace + // Always fallback to empty window return [Object.create(null)]; } @@ -656,16 +779,29 @@ export class WindowsManager implements IWindowsMainService { try { const candidateStat = fs.statSync(candidate); if (candidateStat) { - return candidateStat.isFile() ? - { + if (candidateStat.isFile()) { + + // Workspace + const workspace = this.workspacesService.resolveWorkspaceSync(candidate); + if (workspace) { + return { workspace }; + } + + // File + return { filePath: candidate, lineNumber: gotoLineMode ? parsedPath.line : void 0, columnNumber: gotoLineMode ? parsedPath.column : void 0 - } : - { workspacePath: candidate }; + }; + } + + // Folder + return { + folderPath: candidate + }; } } catch (error) { - this.historyService.removeFromRecentPathsList(candidate); // since file does not seem to exist anymore, remove from recent + this.historyService.removeFromRecentlyOpened(candidate); // since file does not seem to exist anymore, remove from recent if (ignoreFileNotFound) { return { filePath: candidate, createFilePath: true }; // assume this is a file that does not yet exist @@ -717,17 +853,17 @@ export class WindowsManager implements IWindowsMainService { return; } - // Fill in previously opened workspace unless an explicit path is provided and we are not unit testing + // Fill in previously opened folder unless an explicit path is provided and we are not unit testing if (openConfig.cli._.length === 0 && !openConfig.cli.extensionTestsPath) { - const workspaceToOpen = this.windowsState.lastPluginDevelopmentHostWindow && this.windowsState.lastPluginDevelopmentHostWindow.workspacePath; - if (workspaceToOpen) { - openConfig.cli._ = [workspaceToOpen]; + const folderToOpen = this.windowsState.lastPluginDevelopmentHostWindow && this.windowsState.lastPluginDevelopmentHostWindow.folderPath; + if (folderToOpen) { + openConfig.cli._ = [folderToOpen]; } } // Make sure we are not asked to open a path that is already opened if (openConfig.cli._.length > 0) { - res = WindowsManager.WINDOWS.filter(w => w.openedWorkspacePath && openConfig.cli._.indexOf(w.openedWorkspacePath) >= 0); + res = WindowsManager.WINDOWS.filter(w => w.openedFolderPath && openConfig.cli._.indexOf(w.openedFolderPath) >= 0); if (res.length) { openConfig.cli._ = []; } @@ -745,25 +881,24 @@ export class WindowsManager implements IWindowsMainService { configuration.execPath = process.execPath; configuration.userEnv = assign({}, this.initialUserEnv, options.userEnv || {}); configuration.isInitialStartup = options.initialStartup; - configuration.workspacePath = options.workspacePath; + configuration.workspace = options.workspace; + configuration.folderPath = options.folderPath; configuration.filesToOpen = options.filesToOpen; configuration.filesToCreate = options.filesToCreate; configuration.filesToDiff = options.filesToDiff; configuration.nodeCachedDataDir = this.environmentService.nodeCachedDataDir; - // if we know the backup folder upfront (for empty workspaces to restore), we can set it + // if we know the backup folder upfront (for empty windows to restore), we can set it // directly here which helps for restoring UI state associated with that window. - // For all other cases we first call into registerWindowForBackupsSync() to set it before + // For all other cases we first call into registerEmptyWindowBackupSync() to set it before // loading the window. - if (options.emptyWorkspaceBackupFolder) { - configuration.backupPath = path.join(this.environmentService.backupHome, options.emptyWorkspaceBackupFolder); + if (options.emptyWindowBackupFolder) { + configuration.backupPath = path.join(this.environmentService.backupHome, options.emptyWindowBackupFolder); } let codeWindow: CodeWindow; - if (!options.forceNewWindow) { codeWindow = options.windowToUse || this.getLastActiveWindow(); - if (codeWindow) { codeWindow.focus(); } @@ -809,25 +944,6 @@ export class WindowsManager implements IWindowsMainService { codeWindow.win.on('unresponsive', () => this.onWindowError(codeWindow, WindowError.UNRESPONSIVE)); codeWindow.win.on('closed', () => this.onWindowClosed(codeWindow)); - // Prevent loading on svgs in main renderer - codeWindow.win.webContents.session.webRequest.onBeforeRequest((details, callback) => { - if (details.url.indexOf('.svg') > 0) { - const uri = URI.parse(details.url); - if (uri && !uri.scheme.match(/file/i) && (uri.path as any).endsWith('.svg')) { - return callback({ cancel: true }); - } - } - return callback({}); - }); - - codeWindow.win.webContents.session.webRequest.onHeadersReceived((details, callback) => { - const contentType: string[] = (details.responseHeaders['content-type'] || details.responseHeaders['Content-Type']) as any; - if (contentType && Array.isArray(contentType) && contentType.some(x => x.toLowerCase().indexOf('image/svg') >= 0)) { - return callback({ cancel: true }); - } - return callback({ cancel: false, responseHeaders: details.responseHeaders }); - }); - // Lifecycle this.lifecycleService.registerWindow(codeWindow); } @@ -853,8 +969,13 @@ export class WindowsManager implements IWindowsMainService { // Register window for backups if (!configuration.extensionDevelopmentPath) { - const backupPath = this.backupService.registerWindowForBackupsSync(codeWindow.id, !configuration.workspacePath, options.emptyWorkspaceBackupFolder, configuration.workspacePath); - configuration.backupPath = backupPath; + if (configuration.workspace) { + configuration.backupPath = this.backupService.registerWorkspaceBackupSync(configuration.workspace); + } else if (configuration.folderPath) { + configuration.backupPath = this.backupService.registerFolderBackupSync(configuration.folderPath); + } else { + configuration.backupPath = this.backupService.registerEmptyWindowBackupSync(options.emptyWindowBackupFolder); + } } // Load it @@ -872,19 +993,27 @@ export class WindowsManager implements IWindowsMainService { return this.windowsState.lastPluginDevelopmentHostWindow.uiState; } - // Known Folder - load from stored settings if any - if (configuration.workspacePath) { - const stateForWorkspace = this.windowsState.openedWindows.filter(o => isEqual(o.workspacePath, configuration.workspacePath, !isLinux /* ignorecase */)).map(o => o.uiState); + // Known Workspace - load from stored settings + if (configuration.workspace) { + const stateForWorkspace = this.windowsState.openedWindows.filter(o => o.workspace && o.workspace.id === configuration.workspace.id).map(o => o.uiState); if (stateForWorkspace.length) { return stateForWorkspace[0]; } } - // Empty workspace with backups + // Known Folder - load from stored settings + if (configuration.folderPath) { + const stateForFolder = this.windowsState.openedWindows.filter(o => isEqual(o.folderPath, configuration.folderPath, !isLinux /* ignorecase */)).map(o => o.uiState); + if (stateForFolder.length) { + return stateForFolder[0]; + } + } + + // Empty windows with backups else if (configuration.backupPath) { - const stateForWorkspace = this.windowsState.openedWindows.filter(o => o.backupPath === configuration.backupPath).map(o => o.uiState); - if (stateForWorkspace.length) { - return stateForWorkspace[0]; + const stateForEmptyWindow = this.windowsState.openedWindows.filter(o => o.backupPath === configuration.backupPath).map(o => o.uiState); + if (stateForEmptyWindow.length) { + return stateForEmptyWindow[0]; } } @@ -1066,7 +1195,7 @@ export class WindowsManager implements IWindowsMainService { } private onWindowError(codeWindow: CodeWindow, error: WindowError): void { - console.error(error === WindowError.CRASHED ? '[VS Code]: render process crashed!' : '[VS Code]: detected unresponsive'); + this.logService.error(error === WindowError.CRASHED ? '[VS Code]: render process crashed!' : '[VS Code]: detected unresponsive'); // Unresponsive if (error === WindowError.UNRESPONSIVE) { diff --git a/src/vs/code/node/windowsFinder.ts b/src/vs/code/node/windowsFinder.ts index 309decbd6e9..1190f482fee 100644 --- a/src/vs/code/node/windowsFinder.ts +++ b/src/vs/code/node/windowsFinder.ts @@ -10,9 +10,12 @@ import * as fs from 'fs'; import * as platform from 'vs/base/common/platform'; import * as paths from 'vs/base/common/paths'; import { OpenContext } from 'vs/platform/windows/common/windows'; +import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier } from "vs/platform/workspaces/common/workspaces"; +import { isParent } from "vs/platform/files/common/files"; export interface ISimpleWindow { - openedWorkspacePath: string; + openedWorkspace?: IWorkspaceIdentifier; + openedFolderPath?: string; openedFilePath?: string; extensionDevelopmentPath?: string; lastFocusTime: number; @@ -34,7 +37,7 @@ export function findBestWindowOrFolderForFile({ windows const folderWithCodeSettings = !reuseWindow && findFolderWithCodeSettings(filePath, userHome, codeSettingsFolder); // Return if we found a window that has the parent of the file path opened - if (windowOnFilePath && !(folderWithCodeSettings && folderWithCodeSettings.length > windowOnFilePath.openedWorkspacePath.length)) { + if (windowOnFilePath && !(folderWithCodeSettings && folderWithCodeSettings.length > windowOnFilePath.openedFolderPath.length)) { return windowOnFilePath; } @@ -51,9 +54,9 @@ function findWindowOnFilePath(windows: W[], filePath: s // From all windows that have the parent of the file opened, return the window // that has the most specific folder opened ( = longest path wins) - const windowsOnFilePath = windows.filter(window => typeof window.openedWorkspacePath === 'string' && paths.isEqualOrParent(filePath, window.openedWorkspacePath, !platform.isLinux /* ignorecase */)); + const windowsOnFilePath = windows.filter(window => typeof window.openedFolderPath === 'string' && paths.isEqualOrParent(filePath, window.openedFolderPath, !platform.isLinux /* ignorecase */)); if (windowsOnFilePath.length) { - return windowsOnFilePath.sort((a, b) => -(a.openedWorkspacePath.length - b.openedWorkspacePath.length))[0]; + return windowsOnFilePath.sort((a, b) => -(a.openedFolderPath.length - b.openedFolderPath.length))[0]; } return null; @@ -105,22 +108,32 @@ export function getLastActiveWindow(windows: W[]): W { return null; } -export function findWindowOnWorkspace(windows: W[], workspacePath: string): W { +export function findWindowOnFolder(windows: W[], folderPath: string): W { if (windows.length) { + const res = windows.filter(w => { - // Sort the last active window to the front of the array of windows to test - const windowsToTest = windows.slice(0); - const lastActiveWindow = getLastActiveWindow(windows); - if (lastActiveWindow) { - windowsToTest.splice(windowsToTest.indexOf(lastActiveWindow), 1); - windowsToTest.unshift(lastActiveWindow); + // match on folder + if (typeof w.openedFolderPath === 'string' && (paths.isEqual(w.openedFolderPath, folderPath, !platform.isLinux /* ignorecase */))) { + return true; + } + + return false; + }); + + if (res && res.length) { + return res[0]; } + } - // Find it - const res = windowsToTest.filter(w => { + return null; +} + +export function findWindowOnWorkspace(windows: W[], workspace: IWorkspaceIdentifier): W { + if (windows.length) { + const res = windows.filter(w => { // match on workspace - if (typeof w.openedWorkspacePath === 'string' && (paths.isEqual(w.openedWorkspacePath, workspacePath, !platform.isLinux /* ignorecase */))) { + if (w.openedWorkspace && w.openedWorkspace.id === workspace.id) { return true; } @@ -153,4 +166,24 @@ export function findExtensionDevelopmentWindow(windows: } return null; +} + +export function findWindowOnWorkspaceOrFolder(windows: W[], target: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier): W { + const directTargetMatch = windows.filter(w => { + if (typeof target === 'string') { + return paths.isEqual(target, w.openedFolderPath, !platform.isLinux /* ignorecase */); + } + + return w.openedWorkspace && w.openedWorkspace.id === target.id; + }); + + const parentTargetMatch = windows.filter(w => { + if (typeof target === 'string') { + return isParent(target, w.openedFolderPath, !platform.isLinux /* ignorecase */); + } + + return false; // not supported for workspace target + }); + + return directTargetMatch.length ? directTargetMatch[0] : parentTargetMatch[0]; // prefer direct match over parent match } \ No newline at end of file diff --git a/src/vs/code/test/node/windowsUtils.test.ts b/src/vs/code/test/node/windowsUtils.test.ts index 8e42ddd35b7..64bc5cfa906 100644 --- a/src/vs/code/test/node/windowsUtils.test.ts +++ b/src/vs/code/test/node/windowsUtils.test.ts @@ -22,9 +22,9 @@ function options(custom?: Partial>): I }; } -const vscodeFolderWindow = { lastFocusTime: 1, openedWorkspacePath: path.join(fixturesFolder, 'vscode_folder') }; -const lastActiveWindow = { lastFocusTime: 3, openedWorkspacePath: null }; -const noVscodeFolderWindow = { lastFocusTime: 2, openedWorkspacePath: path.join(fixturesFolder, 'no_vscode_folder') }; +const vscodeFolderWindow = { lastFocusTime: 1, openedFolderPath: path.join(fixturesFolder, 'vscode_folder') }; +const lastActiveWindow = { lastFocusTime: 3, openedFolderPath: null }; +const noVscodeFolderWindow = { lastFocusTime: 2, openedFolderPath: path.join(fixturesFolder, 'no_vscode_folder') }; const windows = [ vscodeFolderWindow, lastActiveWindow, @@ -101,7 +101,7 @@ suite('WindowsFinder', () => { }); test('Existing window wins over vscode folder if more specific', () => { - const window = { lastFocusTime: 1, openedWorkspacePath: path.join(fixturesFolder, 'vscode_folder', 'nested_folder') }; + const window = { lastFocusTime: 1, openedFolderPath: path.join(fixturesFolder, 'vscode_folder', 'nested_folder') }; assert.equal(findBestWindowOrFolderForFile(options({ windows: [window], filePath: path.join(fixturesFolder, 'vscode_folder', 'nested_folder', 'subfolder', 'file.txt') @@ -114,8 +114,8 @@ suite('WindowsFinder', () => { }); test('More specific existing window wins', () => { - const window = { lastFocusTime: 2, openedWorkspacePath: path.join(fixturesFolder, 'no_vscode_folder') }; - const nestedFolderWindow = { lastFocusTime: 1, openedWorkspacePath: path.join(fixturesFolder, 'no_vscode_folder', 'nested_folder') }; + const window = { lastFocusTime: 2, openedFolderPath: path.join(fixturesFolder, 'no_vscode_folder') }; + const nestedFolderWindow = { lastFocusTime: 1, openedFolderPath: path.join(fixturesFolder, 'no_vscode_folder', 'nested_folder') }; assert.equal(findBestWindowOrFolderForFile(options({ windows: [window, nestedFolderWindow], filePath: path.join(fixturesFolder, 'no_vscode_folder', 'nested_folder', 'subfolder', 'file.txt') @@ -123,7 +123,7 @@ suite('WindowsFinder', () => { }); test('VSCode folder wins over existing window if more specific', () => { - const window = { lastFocusTime: 1, openedWorkspacePath: path.join(fixturesFolder, 'vscode_folder') }; + const window = { lastFocusTime: 1, openedFolderPath: path.join(fixturesFolder, 'vscode_folder') }; assert.equal(findBestWindowOrFolderForFile(options({ windows: [window], filePath: path.join(fixturesFolder, 'vscode_folder', 'nested_vscode_folder', 'subfolder', 'file.txt') diff --git a/src/vs/editor/browser/controller/mouseTarget.ts b/src/vs/editor/browser/controller/mouseTarget.ts index b91b491be6b..3626e6cfb70 100644 --- a/src/vs/editor/browser/controller/mouseTarget.ts +++ b/src/vs/editor/browser/controller/mouseTarget.ts @@ -67,7 +67,7 @@ interface IETextRange { declare var IETextRange: { prototype: IETextRange; - new (): IETextRange; + new(): IETextRange; }; interface IHitTestResult { diff --git a/src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler.ts b/src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler.ts index f88a38ed4dd..92b52e61df1 100644 --- a/src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler.ts +++ b/src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler.ts @@ -14,7 +14,7 @@ import { TokenizationRegistry } from 'vs/editor/common/modes'; import { IDisposable } from 'vs/base/common/lifecycle'; import * as viewEvents from 'vs/editor/common/view/viewEvents'; import { OverviewRulerZone } from 'vs/editor/common/view/overviewZoneManager'; -import { editorOverviewRulerBorder, editorCursor } from 'vs/editor/common/view/editorColorRegistry'; +import { editorOverviewRulerBorder, editorCursorForeground } from 'vs/editor/common/view/editorColorRegistry'; import { Color } from 'vs/base/common/color'; import { ThemeColor } from 'vs/platform/theme/common/themeService'; @@ -165,7 +165,7 @@ export class DecorationsOverviewRuler extends ViewPart { let borderColor = this._context.theme.getColor(editorOverviewRulerBorder); this._borderColor = borderColor ? borderColor.toString() : null; - let cursorColor = this._context.theme.getColor(editorCursor); + let cursorColor = this._context.theme.getColor(editorCursorForeground); this._cursorColor = cursorColor ? cursorColor.transparent(0.7).toString() : null; this._overviewRuler.setThemeType(this._context.theme.type, false); diff --git a/src/vs/editor/browser/viewParts/viewCursors/viewCursors.ts b/src/vs/editor/browser/viewParts/viewCursors/viewCursors.ts index a5362a124b8..699f706a530 100644 --- a/src/vs/editor/browser/viewParts/viewCursors/viewCursors.ts +++ b/src/vs/editor/browser/viewParts/viewCursors/viewCursors.ts @@ -15,7 +15,7 @@ import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode'; import { TimeoutTimer, IntervalTimer } from 'vs/base/common/async'; import * as viewEvents from 'vs/editor/common/view/viewEvents'; import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; -import { editorCursor } from 'vs/editor/common/view/editorColorRegistry'; +import { editorCursorForeground, editorCursorBackground } from 'vs/editor/common/view/editorColorRegistry'; import { TextEditorCursorBlinkingStyle, TextEditorCursorStyle } from 'vs/editor/common/config/editorOptions'; export class ViewCursors extends ViewPart { @@ -346,12 +346,15 @@ export class ViewCursors extends ViewPart { } registerThemingParticipant((theme, collector) => { - let caret = theme.getColor(editorCursor); + let caret = theme.getColor(editorCursorForeground); if (caret) { - let oppositeCaret = caret.opposite(); - collector.addRule(`.monaco-editor .cursor { background-color: ${caret}; border-color: ${caret}; color: ${oppositeCaret}; }`); + let caretBackground = theme.getColor(editorCursorBackground); + if (!caretBackground) { + caretBackground = caret.opposite(); + } + collector.addRule(`.monaco-editor .cursor { background-color: ${caret}; border-color: ${caret}; color: ${caretBackground}; }`); if (theme.type === 'hc') { - collector.addRule(`.monaco-editor .cursors-layer.has-selection .cursor { border-left: 1px solid ${oppositeCaret}; border-right: 1px solid ${oppositeCaret}; }`); + collector.addRule(`.monaco-editor .cursors-layer.has-selection .cursor { border-left: 1px solid ${caretBackground}; border-right: 1px solid ${caretBackground}; }`); } } diff --git a/src/vs/editor/common/editorCommonExtensions.ts b/src/vs/editor/common/editorCommonExtensions.ts index dfc672ab721..7fc3fda9ce2 100644 --- a/src/vs/editor/common/editorCommonExtensions.ts +++ b/src/vs/editor/common/editorCommonExtensions.ts @@ -94,7 +94,7 @@ export interface IContributionCommandOptions extends ICommandOptions { handler: (controller: T) => void; } export interface EditorControllerCommand { - new (opts: IContributionCommandOptions): EditorCommand; + new(opts: IContributionCommandOptions): EditorCommand; } export abstract class EditorCommand extends Command { @@ -202,11 +202,11 @@ export abstract class EditorAction extends EditorCommand { // --- Registration of commands and actions -export function editorAction(ctor: { new (): EditorAction; }): void { +export function editorAction(ctor: { new(): EditorAction; }): void { CommonEditorRegistry.registerEditorAction(new ctor()); } -export function editorCommand(ctor: { new (): EditorCommand }): void { +export function editorCommand(ctor: { new(): EditorCommand }): void { registerEditorCommand(new ctor()); } diff --git a/src/vs/editor/common/model/wordHelper.ts b/src/vs/editor/common/model/wordHelper.ts index 8ced8f8c91f..05305e0c187 100644 --- a/src/vs/editor/common/model/wordHelper.ts +++ b/src/vs/editor/common/model/wordHelper.ts @@ -57,10 +57,6 @@ export function ensureValidWordDefinition(wordDefinition?: RegExp): RegExp { function getWordAtPosFast(column: number, wordDefinition: RegExp, text: string, textOffset: number): IWordAtPosition { // find whitespace enclosed text around column and match from there - if (wordDefinition.test(' ')) { - return getWordAtPosSlow(column, wordDefinition, text, textOffset); - } - let pos = column - 1 - textOffset; let start = text.lastIndexOf(' ', pos - 1) + 1; let end = text.indexOf(' ', pos); @@ -113,10 +109,25 @@ function getWordAtPosSlow(column: number, wordDefinition: RegExp, text: string, } export function getWordAtText(column: number, wordDefinition: RegExp, text: string, textOffset: number): IWordAtPosition { - const result = getWordAtPosFast(column, wordDefinition, text, textOffset); + + // if `words` can contain whitespace character we have to use the slow variant + // otherwise we use the fast variant of finding a word + wordDefinition.lastIndex = 0; + let match = wordDefinition.exec(text); + if (!match) { + return null; + } + // todo@joh the `match` could already be the (first) word + const ret = match[0].indexOf(' ') >= 0 + // did match a word which contains a space character -> use slow word find + ? getWordAtPosSlow(column, wordDefinition, text, textOffset) + // sane word definition -> use fast word find + : getWordAtPosFast(column, wordDefinition, text, textOffset); + // both (getWordAtPosFast and getWordAtPosSlow) leave the wordDefinition-RegExp // in an undefined state and to not confuse other users of the wordDefinition // we reset the lastIndex wordDefinition.lastIndex = 0; - return result; + + return ret; } diff --git a/src/vs/editor/common/modes.ts b/src/vs/editor/common/modes.ts index 4d85cdfa5ac..4dfb8d54832 100644 --- a/src/vs/editor/common/modes.ts +++ b/src/vs/editor/common/modes.ts @@ -258,13 +258,6 @@ export interface ISuggestSupport { resolveCompletionItem?(model: editorCommon.IModel, position: Position, item: ISuggestion, token: CancellationToken): ISuggestion | Thenable; } -/** - * Interface used to quick fix typing errors while accesing member fields. - */ -export interface CodeAction { - command: Command; - score: number; -} /** * The code action interface defines the contract between extensions and * the [light bulb](https://code.visualstudio.com/docs/editor/editingevolved#_code-action) feature. @@ -274,7 +267,7 @@ export interface CodeActionProvider { /** * Provide commands for the given document and range. */ - provideCodeActions(model: editorCommon.IReadOnlyModel, range: Range, token: CancellationToken): CodeAction[] | Thenable; + provideCodeActions(model: editorCommon.IReadOnlyModel, range: Range, token: CancellationToken): Command[] | Thenable; } /** diff --git a/src/vs/editor/common/view/editorColorRegistry.ts b/src/vs/editor/common/view/editorColorRegistry.ts index 855522bd9fa..05089d2d390 100644 --- a/src/vs/editor/common/view/editorColorRegistry.ts +++ b/src/vs/editor/common/view/editorColorRegistry.ts @@ -14,7 +14,8 @@ import { Color } from 'vs/base/common/color'; export const editorLineHighlight = registerColor('editor.lineHighlightBackground', { dark: null, light: null, hc: null }, nls.localize('lineHighlight', 'Background color for the highlight of line at the cursor position.')); export const editorLineHighlightBorder = registerColor('editor.lineHighlightBorder', { dark: '#282828', light: '#eeeeee', hc: '#f38518' }, nls.localize('lineHighlightBorderBox', 'Background color for the border around the line at the cursor position.')); export const editorRangeHighlight = registerColor('editor.rangeHighlightBackground', { dark: '#ffffff0b', light: '#fdff0033', hc: null }, nls.localize('rangeHighlight', 'Background color of highlighted ranges, like by quick open and find features.')); -export const editorCursor = registerColor('editorCursor.foreground', { dark: '#AEAFAD', light: Color.black, hc: Color.white }, nls.localize('caret', 'Color of the editor cursor.')); +export const editorCursorForeground = registerColor('editorCursor.foreground', { dark: '#AEAFAD', light: Color.black, hc: Color.white }, nls.localize('caret', 'Color of the editor cursor.')); +export const editorCursorBackground = registerColor('editorCursor.background', null, nls.localize('editorCursorBackground', 'The background color of the editor cursor. Allows customizing the color of a character overlapped by a block cursor.')); export const editorWhitespaces = registerColor('editorWhitespace.foreground', { dark: '#e3e4e229', light: '#33333333', hc: '#e3e4e229' }, nls.localize('editorWhitespaces', 'Color of whitespace characters in the editor.')); export const editorIndentGuides = registerColor('editorIndentGuide.background', { dark: editorWhitespaces, light: editorWhitespaces, hc: editorWhitespaces }, nls.localize('editorIndentGuides', 'Color of the editor indentation guides.')); export const editorLineNumbers = registerColor('editorLineNumber.foreground', { dark: '#5A5A5A', light: '#2B91AF', hc: Color.white }, nls.localize('editorLineNumbers', 'Color of editor line numbers.')); diff --git a/src/vs/editor/contrib/find/browser/findWidget.ts b/src/vs/editor/contrib/find/browser/findWidget.ts index ceecbd9db0c..4aeef0a9618 100644 --- a/src/vs/editor/contrib/find/browser/findWidget.ts +++ b/src/vs/editor/contrib/find/browser/findWidget.ts @@ -850,6 +850,8 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas this._domNode = document.createElement('div'); this._domNode.className = 'editor-widget find-widget'; this._domNode.setAttribute('aria-hidden', 'true'); + // We need to set this explicitly, otherwise on IE11, the width inheritence of flex doesn't work. + this._domNode.style.width = `${FIND_WIDGET_INITIAL_WIDTH}px`; this._domNode.appendChild(this._toggleReplaceBtn.domNode); this._domNode.appendChild(findPart); diff --git a/src/vs/editor/contrib/hover/browser/modesContentHover.ts b/src/vs/editor/contrib/hover/browser/modesContentHover.ts index ff8f4d98be7..62de2182e7e 100644 --- a/src/vs/editor/contrib/hover/browser/modesContentHover.ts +++ b/src/vs/editor/contrib/hover/browser/modesContentHover.ts @@ -257,7 +257,7 @@ export class ModesContentHoverWidget extends ContentHoverWidget { : this._editor.getModel().getLanguageIdentifier().language; return this._modeService.getOrCreateMode(modeId).then(_ => { - return `
${tokenizeToString(value, modeId)}
`; + return tokenizeToString(value, modeId); }); } }); diff --git a/src/vs/editor/contrib/hover/browser/modesGlyphHover.ts b/src/vs/editor/contrib/hover/browser/modesGlyphHover.ts index 3180bb7c383..0900532ea51 100644 --- a/src/vs/editor/contrib/hover/browser/modesGlyphHover.ts +++ b/src/vs/editor/contrib/hover/browser/modesGlyphHover.ts @@ -174,7 +174,7 @@ export class ModesGlyphHoverWidget extends GlyphHoverWidget { // In markdown, it is possible that we stumble upon language aliases (e.g. js instead of javascript) const modeId = this.modeService.getModeIdForLanguageName(languageAlias); return this.modeService.getOrCreateMode(modeId).then(_ => { - return `
${tokenizeToString(value, modeId)}
`; + return tokenizeToString(value, modeId); }); } }); diff --git a/src/vs/editor/contrib/quickFix/browser/quickFix.ts b/src/vs/editor/contrib/quickFix/browser/quickFix.ts index 9d50f40a6a8..6f13bbbc48c 100644 --- a/src/vs/editor/contrib/quickFix/browser/quickFix.ts +++ b/src/vs/editor/contrib/quickFix/browser/quickFix.ts @@ -7,20 +7,24 @@ import URI from 'vs/base/common/uri'; import { IReadOnlyModel } from 'vs/editor/common/editorCommon'; import { Range } from 'vs/editor/common/core/range'; -import { CodeAction, CodeActionProviderRegistry } from 'vs/editor/common/modes'; +import { Command, CodeActionProviderRegistry } from 'vs/editor/common/modes'; import { asWinJsPromise } from 'vs/base/common/async'; import { TPromise } from 'vs/base/common/winjs.base'; import { onUnexpectedExternalError, illegalArgument } from 'vs/base/common/errors'; import { IModelService } from 'vs/editor/common/services/modelService'; import { CommonEditorRegistry } from 'vs/editor/common/editorCommonExtensions'; -export function getCodeActions(model: IReadOnlyModel, range: Range): TPromise { +export function getCodeActions(model: IReadOnlyModel, range: Range): TPromise { - const allResults: CodeAction[] = []; + const allResults: Command[] = []; const promises = CodeActionProviderRegistry.all(model).map(support => { return asWinJsPromise(token => support.provideCodeActions(model, range, token)).then(result => { if (Array.isArray(result)) { - allResults.push(...result); + for (const quickFix of result) { + if (quickFix) { + allResults.push(quickFix); + } + } } }, err => { onUnexpectedExternalError(err); diff --git a/src/vs/editor/contrib/quickFix/browser/quickFixCommands.ts b/src/vs/editor/contrib/quickFix/browser/quickFixCommands.ts index 1d210678405..605591babeb 100644 --- a/src/vs/editor/contrib/quickFix/browser/quickFixCommands.ts +++ b/src/vs/editor/contrib/quickFix/browser/quickFixCommands.ts @@ -12,16 +12,11 @@ import { IContextMenuService } from 'vs/platform/contextview/browser/contextView import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IMarkerService } from 'vs/platform/markers/common/markers'; -import { ICommonCodeEditor, IEditorContribution, IReadOnlyModel } from 'vs/editor/common/editorCommon'; +import { ICommonCodeEditor, IEditorContribution } from 'vs/editor/common/editorCommon'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; -import { Range } from 'vs/editor/common/core/range'; import { editorAction, ServicesAccessor, EditorAction } from 'vs/editor/common/editorCommonExtensions'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { editorContribution } from 'vs/editor/browser/editorBrowserExtensions'; -import { CodeAction, CodeActionProviderRegistry } from 'vs/editor/common/modes'; -import { asWinJsPromise } from 'vs/base/common/async'; -import { TPromise } from 'vs/base/common/winjs.base'; -import { onUnexpectedExternalError } from 'vs/base/common/errors'; import { QuickFixContextMenu } from './quickFixWidget'; import { LightBulbWidget } from './lightBulbWidget'; import { QuickFixModel, QuickFixComputeEvent } from './quickFixModel'; @@ -135,21 +130,3 @@ export class QuickFixAction extends EditorAction { } } } - - -export function getCodeActions(model: IReadOnlyModel, range: Range): TPromise { - - const allResults: CodeAction[] = []; - const promises = CodeActionProviderRegistry.all(model).map(support => { - return asWinJsPromise(token => support.provideCodeActions(model, range, token)).then(result => { - if (Array.isArray(result)) { - allResults.push(...result); - } - }, err => { - onUnexpectedExternalError(err); - }); - }); - - return TPromise.join(promises).then(() => allResults); -} - diff --git a/src/vs/editor/contrib/quickFix/browser/quickFixModel.ts b/src/vs/editor/contrib/quickFix/browser/quickFixModel.ts index 4688f618cfd..9d7a64cb615 100644 --- a/src/vs/editor/contrib/quickFix/browser/quickFixModel.ts +++ b/src/vs/editor/contrib/quickFix/browser/quickFixModel.ts @@ -4,15 +4,14 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import * as arrays from 'vs/base/common/arrays'; import Event, { Emitter, debounceEvent } from 'vs/base/common/event'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import URI from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; -import { IMarker, IMarkerService } from 'vs/platform/markers/common/markers'; +import { IMarkerService } from 'vs/platform/markers/common/markers'; import { Range } from 'vs/editor/common/core/range'; import { ICommonCodeEditor } from 'vs/editor/common/editorCommon'; -import { CodeActionProviderRegistry, CodeAction } from 'vs/editor/common/modes'; +import { CodeActionProviderRegistry, Command } from 'vs/editor/common/modes'; import { getCodeActions } from './quickFix'; import { Position } from 'vs/editor/common/core/position'; @@ -39,10 +38,21 @@ export class QuickFixOracle { } trigger(type: 'manual' | 'auto'): void { - let range = this._rangeAtPosition(); - if (!range) { - range = this._editor.getSelection(); + + // get selection from marker or current word + // unless the selection is non-empty and manually + // requesting code actions + let selection = this._editor.getSelection(); + let range = this._getActiveMarkerOrWordRange(); + if (type === 'manual' && !selection.isEmpty()) { + range = selection; } + + // empty selection somewhere in nowhere + if (!range) { + range = selection; + } + this._signalChange({ type, range, @@ -64,7 +74,7 @@ export class QuickFixOracle { } private _onCursorChange(): void { - const range = this._rangeAtPosition(); + const range = this._getActiveMarkerOrWordRange(); if (!Range.equalsRange(this._currentRange, range)) { this._currentRange = range; this._signalChange({ @@ -76,48 +86,28 @@ export class QuickFixOracle { } } - private _rangeAtPosition(): Range { + private _getActiveMarkerOrWordRange(): Range { - // (1) check with non empty selection const selection = this._editor.getSelection(); - if (!selection.isEmpty()) { - return selection; - } - - // (2) check with diagnostics markers - const marker = this._markerAtPosition(); - if (marker) { - return Range.lift(marker); - } - - // (3) check with word - return this._wordAtPosition(); - } - - private _markerAtPosition(): IMarker { - - const position = this._editor.getPosition(); - const { uri } = this._editor.getModel(); - const markers = this._markerService.read({ resource: uri }).sort(Range.compareRangesUsingStarts); - - let idx = arrays.findFirst(markers, marker => marker.endLineNumber >= position.lineNumber); - while (idx < markers.length && markers[idx].endLineNumber >= position.lineNumber) { - const marker = markers[idx]; - if (Range.containsPosition(marker, position)) { - return marker; - } - idx++; - } - return undefined; - } - - private _wordAtPosition(): Range { - const pos = this._editor.getPosition(); const model = this._editor.getModel(); - const info = model.getWordAtPosition(pos); - if (info) { - return new Range(pos.lineNumber, info.startColumn, pos.lineNumber, info.endColumn); + + // (1) return marker that contains a (empty/non-empty) selection + for (const marker of this._markerService.read({ resource: model.uri })) { + const range = Range.lift(marker); + if (range.containsRange(selection)) { + return range; + } } + + // (2) return range of current word + if (selection.isEmpty()) { + const pos = selection.getStartPosition(); + const info = model.getWordAtPosition(pos); + if (info) { + return new Range(pos.lineNumber, info.startColumn, pos.lineNumber, info.endColumn); + } + } + return undefined; } } @@ -126,7 +116,7 @@ export interface QuickFixComputeEvent { type: 'auto' | 'manual'; range: Range; position: Position; - fixes: TPromise; + fixes: TPromise; } export class QuickFixModel { diff --git a/src/vs/editor/contrib/quickFix/browser/quickFixWidget.ts b/src/vs/editor/contrib/quickFix/browser/quickFixWidget.ts index 8c3b5cff522..6cd9f41c1b7 100644 --- a/src/vs/editor/contrib/quickFix/browser/quickFixWidget.ts +++ b/src/vs/editor/contrib/quickFix/browser/quickFixWidget.ts @@ -10,7 +10,7 @@ import { always } from 'vs/base/common/async'; import { getDomNodePagePosition } from 'vs/base/browser/dom'; import { Position } from 'vs/editor/common/core/position'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { CodeAction } from 'vs/editor/common/modes'; +import { Command } from 'vs/editor/common/modes'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { Action } from 'vs/base/common/actions'; @@ -32,10 +32,10 @@ export class QuickFixContextMenu { this._commandService = commandService; } - show(fixes: TPromise, at: { x: number; y: number } | Position) { + show(fixes: TPromise, at: { x: number; y: number } | Position) { const actions = fixes.then(value => { - return value.map(({ command }) => { + return value.map(command => { return new Action(command.id, command.title, undefined, true, () => { return always( this._commandService.executeCommand(command.id, ...command.arguments), diff --git a/src/vs/editor/contrib/quickFix/test/browser/quickFixModel.test.ts b/src/vs/editor/contrib/quickFix/test/browser/quickFixModel.test.ts index fd3484ef05f..6ec07062d99 100644 --- a/src/vs/editor/contrib/quickFix/test/browser/quickFixModel.test.ts +++ b/src/vs/editor/contrib/quickFix/test/browser/quickFixModel.test.ts @@ -38,7 +38,7 @@ suite('QuickFix', () => { setup(() => { reg = CodeActionProviderRegistry.register(languageIdentifier.language, { provideCodeActions() { - return [{ command: { id: 'test-command', title: 'test', arguments: [] }, score: 1 }]; + return [{ id: 'test-command', title: 'test', arguments: [] }]; } }); markerService = new MarkerService(); @@ -182,11 +182,11 @@ suite('QuickFix', () => { return TPromise.join([TPromise.timeout(20)].concat(fixes)).then(_ => { - // assert selection - assert.deepEqual(range, { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 13 }); + // 'auto' triggered, non-empty selection + assert.equal(range, undefined); - range = undefined; - editor.setSelection({ startLineNumber: 1, startColumn: 2, endLineNumber: 1, endColumn: 2 }); + // 'auto' triggered, non-empty selection BUT within a marker + editor.setSelection({ startLineNumber: 1, startColumn: 2, endLineNumber: 1, endColumn: 4 }); return TPromise.join([TPromise.timeout(20)].concat(fixes)).then(_ => { reg.dispose(); @@ -198,4 +198,55 @@ suite('QuickFix', () => { }); }); + test('Lightbulb is in the wrong place, #29933', async function () { + let reg = CodeActionProviderRegistry.register(languageIdentifier.language, { + provideCodeActions(doc, _range) { + return []; + } + }); + + editor.getModel().setValue('// @ts-check\n2\ncon\n'); + + markerService.changeOne('fake', uri, [{ + startLineNumber: 3, startColumn: 1, endLineNumber: 3, endColumn: 4, + message: 'error', + severity: 1, + code: '', + source: '' + }]); + + // case 1 - drag selection over multiple lines -> no automatic lightbulb + await new TPromise(resolve => { + + let oracle = new QuickFixOracle(editor, markerService, e => { + assert.equal(e.type, 'auto'); + assert.equal(e.range, undefined); + + oracle.dispose(); + resolve(null); + }, 5); + + editor.setSelection({ startLineNumber: 1, startColumn: 1, endLineNumber: 4, endColumn: 1 }); + }); + + // case 2 - selection over multiple lines & manual trigger -> lightbulb + await new TPromise(resolve => { + + editor.setSelection({ startLineNumber: 1, startColumn: 1, endLineNumber: 4, endColumn: 1 }); + + let oracle = new QuickFixOracle(editor, markerService, e => { + assert.equal(e.type, 'manual'); + assert.ok(e.range.equalsRange({ startLineNumber: 1, startColumn: 1, endLineNumber: 4, endColumn: 1 })); + + oracle.dispose(); + resolve(null); + }, 5); + + oracle.trigger('manual'); + }); + + + reg.dispose(); + }); + }); diff --git a/src/vs/editor/contrib/snippet/browser/snippetParser.ts b/src/vs/editor/contrib/snippet/browser/snippetParser.ts index c97a9e061db..942fb8358b6 100644 --- a/src/vs/editor/contrib/snippet/browser/snippetParser.ts +++ b/src/vs/editor/contrib/snippet/browser/snippetParser.ts @@ -152,9 +152,12 @@ export abstract class Marker { toString() { return ''; } + len(): number { return 0; } + + abstract clone(): Marker; } export class Text extends Marker { @@ -167,6 +170,9 @@ export class Text extends Marker { len(): number { return this.string.length; } + clone(): Text { + return new Text(this.string); + } } export class Placeholder extends Marker { @@ -197,6 +203,9 @@ export class Placeholder extends Marker { toString() { return Marker.toString(this.children); } + clone(): Placeholder { + return new Placeholder(this.index, this.children.map(child => child.clone())); + } } export class Variable extends Marker { @@ -220,6 +229,11 @@ export class Variable extends Marker { toString() { return this.isDefined ? this.resolvedValue : Marker.toString(this.children); } + clone(): Variable { + const ret = new Variable(this.name, this.children.map(child => child.clone())); + ret.resolvedValue = this.resolvedValue; + return ret; + } } export function walk(marker: Marker[], visitor: (marker: Marker) => boolean): void { const stack = [...marker]; @@ -321,6 +335,10 @@ export class TextmateSnippet extends Marker { parent.children = newChildren; this._placeholders = undefined; } + + clone(): TextmateSnippet { + return new TextmateSnippet(this.children.map(child => child.clone())); + } } export class SnippetParser { @@ -369,7 +387,7 @@ export class SnippetParser { } else if (thisMarker.children.length === 0) { // copy children from first placeholder definition, no need to // recurse on them because they have been visited already - thisMarker.children = placeholderDefaultValues.get(thisMarker.index).slice(0); + thisMarker.children = placeholderDefaultValues.get(thisMarker.index).map(child => child.clone()); } diff --git a/src/vs/editor/contrib/snippet/test/browser/snippetParser.test.ts b/src/vs/editor/contrib/snippet/test/browser/snippetParser.test.ts index 4879ac3b9f8..fc1d8d938c4 100644 --- a/src/vs/editor/contrib/snippet/test/browser/snippetParser.test.ts +++ b/src/vs/editor/contrib/snippet/test/browser/snippetParser.test.ts @@ -393,4 +393,23 @@ suite('SnippetParser', () => { test('Maximum call stack size exceeded, #28983', function () { new SnippetParser().parse('${1:${foo:${1}}}'); }); + + test('Snippet can freeze the editor, #30407', function () { + + const seen = new Set(); + + seen.clear(); + walk(new SnippetParser().parse('class ${1:${TM_FILENAME/(?:\\A|_)([A-Za-z0-9]+)(?:\\.rb)?/(?2::\\u$1)/g}} < ${2:Application}Controller\n $3\nend'), marker => { + assert.ok(!seen.has(marker)); + seen.add(marker); + return true; + }); + + seen.clear(); + walk(new SnippetParser().parse('${1:${FOO:abc$1def}}'), marker => { + assert.ok(!seen.has(marker)); + seen.add(marker); + return true; + }); + }); }); diff --git a/src/vs/editor/standalone/browser/accessibilityHelp/accessibilityHelp.ts b/src/vs/editor/standalone/browser/accessibilityHelp/accessibilityHelp.ts index b3831c11e04..6b3bf89fa91 100644 --- a/src/vs/editor/standalone/browser/accessibilityHelp/accessibilityHelp.ts +++ b/src/vs/editor/standalone/browser/accessibilityHelp/accessibilityHelp.ts @@ -299,6 +299,8 @@ class AccessibilityHelpWidget extends Widget implements IOverlayWidget { text += '\n\n' + nls.localize("outroMsg", "You can dismiss this tooltip and return to the editor by pressing Escape or Shift+Escape."); this._contentDomNode.domNode.appendChild(renderFormattedText(text)); + // Per https://www.w3.org/TR/wai-aria/roles#document, Authors SHOULD provide a title or label for documents + this._contentDomNode.domNode.setAttribute('aria-label', text); } public hide(): void { diff --git a/src/vs/editor/standalone/browser/simpleServices.ts b/src/vs/editor/standalone/browser/simpleServices.ts index b39ed672ee0..dcae68b3d18 100644 --- a/src/vs/editor/standalone/browser/simpleServices.ts +++ b/src/vs/editor/standalone/browser/simpleServices.ts @@ -548,6 +548,14 @@ export class SimpleWorkspaceContextService implements IWorkspaceContextService { return true; } + public hasFolderWorkspace(): boolean { + return true; + } + + public hasMultiFolderWorkspace(): boolean { + return false; + } + public isInsideWorkspace(resource: URI): boolean { return resource && resource.scheme === SimpleWorkspaceContextService.SCHEME; } diff --git a/src/vs/editor/standalone/browser/standaloneLanguages.ts b/src/vs/editor/standalone/browser/standaloneLanguages.ts index 17648894450..833bcf2d4d7 100644 --- a/src/vs/editor/standalone/browser/standaloneLanguages.ts +++ b/src/vs/editor/standalone/browser/standaloneLanguages.ts @@ -329,7 +329,7 @@ export function registerCodeLensProvider(languageId: string, provider: modes.Cod */ export function registerCodeActionProvider(languageId: string, provider: CodeActionProvider): IDisposable { return modes.CodeActionProviderRegistry.register(languageId, { - provideCodeActions: (model: editorCommon.IReadOnlyModel, range: Range, token: CancellationToken): modes.CodeAction[] | Thenable => { + provideCodeActions: (model: editorCommon.IReadOnlyModel, range: Range, token: CancellationToken): modes.Command[] | Thenable => { let markers = StaticServices.markerService.get().read({ resource: model.uri }).filter(m => { return Range.areIntersectingOrTouching(m, range); }); @@ -404,7 +404,7 @@ export interface CodeActionProvider { /** * Provide commands for the given document and range. */ - provideCodeActions(model: editorCommon.IReadOnlyModel, range: Range, context: CodeActionContext, token: CancellationToken): modes.CodeAction[] | Thenable; + provideCodeActions(model: editorCommon.IReadOnlyModel, range: Range, context: CodeActionContext, token: CancellationToken): modes.Command[] | Thenable; } /** diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 7e3864f54b5..0fc13a41c86 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -4050,7 +4050,7 @@ declare module monaco.languages { /** * Provide commands for the given document and range. */ - provideCodeActions(model: editor.IReadOnlyModel, range: Range, context: CodeActionContext, token: CancellationToken): CodeAction[] | Thenable; + provideCodeActions(model: editor.IReadOnlyModel, range: Range, context: CodeActionContext, token: CancellationToken): Command[] | Thenable; } /** @@ -4425,14 +4425,6 @@ declare module monaco.languages { provideHover(model: editor.IReadOnlyModel, position: Position, token: CancellationToken): Hover | Thenable; } - /** - * Interface used to quick fix typing errors while accesing member fields. - */ - export interface CodeAction { - command: Command; - score: number; - } - /** * Represents a parameter of a callable-signature. A parameter can * have a label and a doc-comment. diff --git a/src/vs/platform/backup/common/backup.ts b/src/vs/platform/backup/common/backup.ts index dcc3eec908c..622e52b1ad4 100644 --- a/src/vs/platform/backup/common/backup.ts +++ b/src/vs/platform/backup/common/backup.ts @@ -4,8 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { IWorkspaceIdentifier } from "vs/platform/workspaces/common/workspaces"; export interface IBackupWorkspacesFormat { + rootWorkspaces: IWorkspaceIdentifier[]; folderWorkspaces: string[]; emptyWorkspaces: string[]; } @@ -15,8 +17,11 @@ export const IBackupMainService = createDecorator('backupMai export interface IBackupMainService { _serviceBrand: any; - getWorkspaceBackupPaths(): string[]; - getEmptyWorkspaceBackupPaths(): string[]; + getWorkspaceBackups(): IWorkspaceIdentifier[]; + getFolderBackupPaths(): string[]; + getEmptyWindowBackupPaths(): string[]; - registerWindowForBackupsSync(windowId: number, isEmptyWorkspace: boolean, backupFolder?: string, workspacePath?: string): string; + registerWorkspaceBackupSync(workspace: IWorkspaceIdentifier): string; + registerFolderBackupSync(folderPath: string): string; + registerEmptyWindowBackupSync(backupFolder?: string): string; } \ No newline at end of file diff --git a/src/vs/platform/backup/electron-main/backupMainService.ts b/src/vs/platform/backup/electron-main/backupMainService.ts index 59f9d014a8c..ca56d0d6275 100644 --- a/src/vs/platform/backup/electron-main/backupMainService.ts +++ b/src/vs/platform/backup/electron-main/backupMainService.ts @@ -13,6 +13,8 @@ import { IBackupWorkspacesFormat, IBackupMainService } from 'vs/platform/backup/ import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IFilesConfiguration, HotExitConfiguration } from 'vs/platform/files/common/files'; +import { ILogService } from "vs/platform/log/common/log"; +import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier } from "vs/platform/workspaces/common/workspaces"; export class BackupMainService implements IBackupMainService { @@ -21,11 +23,12 @@ export class BackupMainService implements IBackupMainService { protected backupHome: string; protected workspacesJsonPath: string; - private backups: IBackupWorkspacesFormat; + protected backups: IBackupWorkspacesFormat; constructor( @IEnvironmentService environmentService: IEnvironmentService, - @IConfigurationService private configurationService: IConfigurationService + @IConfigurationService private configurationService: IConfigurationService, + @ILogService private logService: ILogService ) { this.backupHome = environmentService.backupHome; this.workspacesJsonPath = environmentService.backupWorkspacesPath; @@ -33,9 +36,17 @@ export class BackupMainService implements IBackupMainService { this.loadSync(); } - public getWorkspaceBackupPaths(): string[] { - const config = this.configurationService.getConfiguration(); - if (config && config.files && config.files.hotExit === HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE) { + public getWorkspaceBackups(): IWorkspaceIdentifier[] { + if (this.isHotExitOnExitAndWindowClose()) { + // Only non-folder windows are restored on main process launch when + // hot exit is configured as onExitAndWindowClose. + return []; + } + return this.backups.rootWorkspaces.slice(0); // return a copy + } + + public getFolderBackupPaths(): string[] { + if (this.isHotExitOnExitAndWindowClose()) { // Only non-folder windows are restored on main process launch when // hot exit is configured as onExitAndWindowClose. return []; @@ -43,58 +54,75 @@ export class BackupMainService implements IBackupMainService { return this.backups.folderWorkspaces.slice(0); // return a copy } - public getEmptyWorkspaceBackupPaths(): string[] { + private isHotExitOnExitAndWindowClose(): boolean { + const config = this.configurationService.getConfiguration(); + + return config && config.files && config.files.hotExit === HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE; + } + + public getEmptyWindowBackupPaths(): string[] { return this.backups.emptyWorkspaces.slice(0); // return a copy } - public registerWindowForBackupsSync(windowId: number, isEmptyWorkspace: boolean, backupFolder?: string, workspacePath?: string): string { + public registerWorkspaceBackupSync(workspace: IWorkspaceIdentifier): string { + this.pushBackupPathsSync(workspace, this.backups.rootWorkspaces); + + return path.join(this.backupHome, workspace.id); + } + + public registerFolderBackupSync(folderPath: string): string { + this.pushBackupPathsSync(folderPath, this.backups.folderWorkspaces); + + return path.join(this.backupHome, this.getFolderHash(folderPath)); + } + + public registerEmptyWindowBackupSync(backupFolder?: string): string { // Generate a new folder if this is a new empty workspace - if (isEmptyWorkspace && !backupFolder) { - backupFolder = this.getRandomEmptyWorkspaceId(); + if (!backupFolder) { + backupFolder = this.getRandomEmptyWindowId(); } - this.pushBackupPathsSync(isEmptyWorkspace ? backupFolder : workspacePath, isEmptyWorkspace); + this.pushBackupPathsSync(backupFolder, this.backups.emptyWorkspaces); - return path.join(this.backupHome, isEmptyWorkspace ? backupFolder : this.getWorkspaceHash(workspacePath)); + return path.join(this.backupHome, backupFolder); } - private pushBackupPathsSync(workspaceIdentifier: string, isEmptyWorkspace: boolean): string { - const array = isEmptyWorkspace ? this.backups.emptyWorkspaces : this.backups.folderWorkspaces; - if (this.indexOf(workspaceIdentifier, isEmptyWorkspace) === -1) { - array.push(workspaceIdentifier); + private pushBackupPathsSync(workspaceIdentifier: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier, target: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier)[]): void { + if (this.indexOf(workspaceIdentifier, target) === -1) { + target.push(workspaceIdentifier); this.saveSync(); } - - return workspaceIdentifier; } - protected removeBackupPathSync(workspaceIdentifier: string, isEmptyWorkspace: boolean): void { - const array = isEmptyWorkspace ? this.backups.emptyWorkspaces : this.backups.folderWorkspaces; - if (!array) { + protected removeBackupPathSync(workspaceIdentifier: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier, target: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier)[]): void { + if (!target) { return; } - const index = this.indexOf(workspaceIdentifier, isEmptyWorkspace); + const index = this.indexOf(workspaceIdentifier, target); if (index === -1) { return; } - array.splice(index, 1); + target.splice(index, 1); this.saveSync(); } - private indexOf(workspaceIdentifier: string, isEmptyWorkspace: boolean): number { - const array = isEmptyWorkspace ? this.backups.emptyWorkspaces : this.backups.folderWorkspaces; - if (!array) { + private indexOf(workspaceIdentifier: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier, target: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier)[]): number { + if (!target) { return -1; } - if (isEmptyWorkspace) { - return array.indexOf(workspaceIdentifier); + const sanitizedWorkspaceIdentifier = this.sanitizeId(workspaceIdentifier); + + return arrays.firstIndex(target, id => this.sanitizeId(id) === sanitizedWorkspaceIdentifier); + } + + private sanitizeId(workspaceIdentifier: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier): string { + if (typeof workspaceIdentifier === 'string') { + return this.sanitizePath(workspaceIdentifier); } - // for backup workspaces, sanitize the workspace identifier to accomodate for case insensitive file systems - const sanitizedWorkspaceIdentifier = this.sanitizePath(workspaceIdentifier); - return arrays.firstIndex(array, id => this.sanitizePath(id) === sanitizedWorkspaceIdentifier); + return workspaceIdentifier.id; } protected loadSync(): void { @@ -105,6 +133,16 @@ export class BackupMainService implements IBackupMainService { backups = Object.create(null); } + // Ensure rootWorkspaces is a object[] + if (backups.rootWorkspaces) { + const rws = backups.rootWorkspaces; + if (!Array.isArray(rws) || rws.some(r => typeof r !== 'object')) { + backups.rootWorkspaces = []; + } + } else { + backups.rootWorkspaces = []; + } + // Ensure folderWorkspaces is a string[] if (backups.folderWorkspaces) { const fws = backups.folderWorkspaces; @@ -125,67 +163,76 @@ export class BackupMainService implements IBackupMainService { backups.emptyWorkspaces = []; } - this.backups = this.dedupeFolderWorkspaces(backups); + this.backups = this.dedupeBackups(backups); // Validate backup workspaces this.validateBackupWorkspaces(backups); } - protected dedupeFolderWorkspaces(backups: IBackupWorkspacesFormat): IBackupWorkspacesFormat { - // De-duplicate folder workspaces, don't worry about cleaning them up any duplicates as + protected dedupeBackups(backups: IBackupWorkspacesFormat): IBackupWorkspacesFormat { + + // De-duplicate folder/workspace backups. don't worry about cleaning them up any duplicates as // they will be removed when there are no backups. backups.folderWorkspaces = arrays.distinct(backups.folderWorkspaces, ws => this.sanitizePath(ws)); + backups.rootWorkspaces = arrays.distinct(backups.rootWorkspaces, ws => this.sanitizePath(ws.id)); return backups; } private validateBackupWorkspaces(backups: IBackupWorkspacesFormat): void { - const staleBackupWorkspaces: { workspaceIdentifier: string; backupPath: string; isEmptyWorkspace: boolean }[] = []; + const staleBackupWorkspaces: { workspaceIdentifier: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier; backupPath: string; target: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier)[] }[] = []; - // Validate Folder Workspaces - backups.folderWorkspaces.forEach(workspacePath => { - const backupPath = path.join(this.backupHome, this.getWorkspaceHash(workspacePath)); + const workspaceAndFolders: { workspaceIdentifier: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier, target: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier)[] }[] = []; + workspaceAndFolders.push(...backups.rootWorkspaces.map(r => ({ workspaceIdentifier: r, target: backups.rootWorkspaces }))); + workspaceAndFolders.push(...backups.folderWorkspaces.map(f => ({ workspaceIdentifier: f, target: backups.folderWorkspaces }))); + + // Validate Workspace and Folder Backups + workspaceAndFolders.forEach(workspaceOrFolder => { + const workspaceId = workspaceOrFolder.workspaceIdentifier; + const workspacePath = typeof workspaceId === 'string' ? workspaceId : workspaceId.configPath; + const backupPath = path.join(this.backupHome, typeof workspaceId === 'string' ? this.getFolderHash(workspaceId) : workspaceId.id); const hasBackups = this.hasBackupsSync(backupPath); const missingWorkspace = hasBackups && !fs.existsSync(workspacePath); - // If the folder has no backups, make sure to delete it - // If the folder has backups, but the target workspace is missing, convert backups to empty ones + // If the workspace/folder has no backups, make sure to delete it + // If the workspace/folder has backups, but the target workspace is missing, convert backups to empty ones if (!hasBackups || missingWorkspace) { - staleBackupWorkspaces.push({ workspaceIdentifier: workspacePath, backupPath, isEmptyWorkspace: false }); + staleBackupWorkspaces.push({ workspaceIdentifier: workspaceId, backupPath, target: workspaceOrFolder.target }); if (missingWorkspace) { - const identifier = this.pushBackupPathsSync(this.getRandomEmptyWorkspaceId(), true /* is empty workspace */); - const newEmptyWorkspaceBackupPath = path.join(path.dirname(backupPath), identifier); + const identifier = this.getRandomEmptyWindowId(); + this.pushBackupPathsSync(identifier, this.backups.emptyWorkspaces); + const newEmptyWindowBackupPath = path.join(path.dirname(backupPath), identifier); try { - fs.renameSync(backupPath, newEmptyWorkspaceBackupPath); + fs.renameSync(backupPath, newEmptyWindowBackupPath); } catch (ex) { - console.error(`Backup: Could not rename backup folder for missing workspace: ${ex.toString()}`); + this.logService.error(`Backup: Could not rename backup folder for missing workspace: ${ex.toString()}`); - this.removeBackupPathSync(identifier, true); + this.removeBackupPathSync(identifier, this.backups.emptyWorkspaces); } } } }); - // Validate Empty Workspaces + // Validate Empty Windows backups.emptyWorkspaces.forEach(backupFolder => { const backupPath = path.join(this.backupHome, backupFolder); if (!this.hasBackupsSync(backupPath)) { - staleBackupWorkspaces.push({ workspaceIdentifier: backupFolder, backupPath, isEmptyWorkspace: true }); + staleBackupWorkspaces.push({ workspaceIdentifier: backupFolder, backupPath, target: backups.emptyWorkspaces }); } }); // Clean up stale backups staleBackupWorkspaces.forEach(staleBackupWorkspace => { - const { backupPath, workspaceIdentifier, isEmptyWorkspace } = staleBackupWorkspace; + const { backupPath, workspaceIdentifier, target } = staleBackupWorkspace; try { extfs.delSync(backupPath); } catch (ex) { - console.error(`Backup: Could not delete stale backup: ${ex.toString()}`); + this.logService.error(`Backup: Could not delete stale backup: ${ex.toString()}`); } - this.removeBackupPathSync(workspaceIdentifier, isEmptyWorkspace); + this.removeBackupPathSync(workspaceIdentifier, target); }); } @@ -216,11 +263,11 @@ export class BackupMainService implements IBackupMainService { } fs.writeFileSync(this.workspacesJsonPath, JSON.stringify(this.backups)); } catch (ex) { - console.error(`Backup: Could not save workspaces.json: ${ex.toString()}`); + this.logService.error(`Backup: Could not save workspaces.json: ${ex.toString()}`); } } - private getRandomEmptyWorkspaceId(): string { + private getRandomEmptyWindowId(): string { return (Date.now() + Math.round(Math.random() * 1000)).toString(); } @@ -228,7 +275,7 @@ export class BackupMainService implements IBackupMainService { return platform.isLinux ? p : p.toLowerCase(); } - protected getWorkspaceHash(workspacePath: string): string { - return crypto.createHash('md5').update(this.sanitizePath(workspacePath)).digest('hex'); + protected getFolderHash(folderPath: string): string { + return crypto.createHash('md5').update(this.sanitizePath(folderPath)).digest('hex'); } } diff --git a/src/vs/platform/backup/test/electron-main/backupMainService.test.ts b/src/vs/platform/backup/test/electron-main/backupMainService.test.ts index 9519702d03e..f938831e197 100644 --- a/src/vs/platform/backup/test/electron-main/backupMainService.test.ts +++ b/src/vs/platform/backup/test/electron-main/backupMainService.test.ts @@ -19,10 +19,14 @@ import { BackupMainService } from 'vs/platform/backup/electron-main/backupMainSe import { IBackupWorkspacesFormat } from 'vs/platform/backup/common/backup'; import { HotExitConfiguration } from 'vs/platform/files/common/files'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; +import { LogMainService } from "vs/platform/log/common/log"; +import { IWorkspaceIdentifier } from "vs/platform/workspaces/common/workspaces"; +import { createHash } from "crypto"; class TestBackupMainService extends BackupMainService { + constructor(backupHome: string, backupWorkspacesPath: string, configService: TestConfigurationService) { - super(new EnvironmentService(parseArgs(process.argv), process.execPath), configService); + super(new EnvironmentService(parseArgs(process.argv), process.execPath), configService, new LogMainService(new EnvironmentService(parseArgs(process.argv), process.execPath))); this.backupHome = backupHome; this.workspacesJsonPath = backupWorkspacesPath; @@ -31,27 +35,42 @@ class TestBackupMainService extends BackupMainService { this.loadSync(); } - public removeBackupPathSync(workspaceIdenfitier: string, isEmptyWorkspace: boolean): void { - return super.removeBackupPathSync(workspaceIdenfitier, isEmptyWorkspace); + public get backupsData(): IBackupWorkspacesFormat { + return this.backups; + } + + public removeBackupPathSync(workspaceIdentifier: string | IWorkspaceIdentifier, target: (string | IWorkspaceIdentifier)[]): void { + return super.removeBackupPathSync(workspaceIdentifier, target); } public loadSync(): void { super.loadSync(); } - public dedupeFolderWorkspaces(backups: IBackupWorkspacesFormat): IBackupWorkspacesFormat { - return super.dedupeFolderWorkspaces(backups); + public dedupeBackups(backups: IBackupWorkspacesFormat): IBackupWorkspacesFormat { + return super.dedupeBackups(backups); } public toBackupPath(workspacePath: string): string { - return path.join(this.backupHome, super.getWorkspaceHash(workspacePath)); + return path.join(this.backupHome, super.getFolderHash(workspacePath)); } - public getWorkspaceHash(workspacePath: string): string { - return super.getWorkspaceHash(workspacePath); + public getFolderHash(folderPath: string): string { + return super.getFolderHash(folderPath); } } +function toWorkspace(path: string): IWorkspaceIdentifier { + return { + id: createHash('md5').update(sanitizePath(path)).digest('hex'), + configPath: path + }; +} + +function sanitizePath(p: string): string { + return platform.isLinux ? p : p.toLowerCase(); +} + suite('BackupMainService', () => { const parentDir = path.join(os.tmpdir(), 'vsctests', 'service'); const backupHome = path.join(parentDir, 'Backups'); @@ -79,21 +98,21 @@ suite('BackupMainService', () => { extfs.del(backupHome, os.tmpdir(), done); }); - test('service validates backup workspaces on startup and cleans up', done => { + test('service validates backup workspaces on startup and cleans up (folder workspaces)', done => { // 1) backup workspace path does not exist - service.registerWindowForBackupsSync(1, false, null, fooFile.fsPath); - service.registerWindowForBackupsSync(2, false, null, barFile.fsPath); + service.registerFolderBackupSync(fooFile.fsPath); + service.registerFolderBackupSync(barFile.fsPath); service.loadSync(); - assert.deepEqual(service.getWorkspaceBackupPaths(), []); + assert.deepEqual(service.getFolderBackupPaths(), []); // 2) backup workspace path exists with empty contents within fs.mkdirSync(service.toBackupPath(fooFile.fsPath)); fs.mkdirSync(service.toBackupPath(barFile.fsPath)); - service.registerWindowForBackupsSync(1, false, null, fooFile.fsPath); - service.registerWindowForBackupsSync(2, false, null, barFile.fsPath); + service.registerFolderBackupSync(fooFile.fsPath); + service.registerFolderBackupSync(barFile.fsPath); service.loadSync(); - assert.deepEqual(service.getWorkspaceBackupPaths(), []); + assert.deepEqual(service.getFolderBackupPaths(), []); assert.ok(!fs.exists(service.toBackupPath(fooFile.fsPath))); assert.ok(!fs.exists(service.toBackupPath(barFile.fsPath))); @@ -102,10 +121,10 @@ suite('BackupMainService', () => { fs.mkdirSync(service.toBackupPath(barFile.fsPath)); fs.mkdirSync(path.join(service.toBackupPath(fooFile.fsPath), 'file')); fs.mkdirSync(path.join(service.toBackupPath(barFile.fsPath), 'untitled')); - service.registerWindowForBackupsSync(1, false, null, fooFile.fsPath); - service.registerWindowForBackupsSync(2, false, null, barFile.fsPath); + service.registerFolderBackupSync(fooFile.fsPath); + service.registerFolderBackupSync(barFile.fsPath); service.loadSync(); - assert.deepEqual(service.getWorkspaceBackupPaths(), []); + assert.deepEqual(service.getFolderBackupPaths(), []); assert.ok(!fs.exists(service.toBackupPath(fooFile.fsPath))); assert.ok(!fs.exists(service.toBackupPath(barFile.fsPath))); @@ -115,126 +134,226 @@ suite('BackupMainService', () => { fs.mkdirSync(service.toBackupPath(fooFile.fsPath)); fs.mkdirSync(service.toBackupPath(barFile.fsPath)); fs.mkdirSync(fileBackups); - service.registerWindowForBackupsSync(1, false, null, fooFile.fsPath); - assert.equal(service.getWorkspaceBackupPaths().length, 1); - assert.equal(service.getEmptyWorkspaceBackupPaths().length, 0); + service.registerFolderBackupSync(fooFile.fsPath); + assert.equal(service.getFolderBackupPaths().length, 1); + assert.equal(service.getEmptyWindowBackupPaths().length, 0); fs.writeFileSync(path.join(fileBackups, 'backup.txt'), ''); service.loadSync(); - assert.equal(service.getWorkspaceBackupPaths().length, 0); - assert.equal(service.getEmptyWorkspaceBackupPaths().length, 1); + assert.equal(service.getFolderBackupPaths().length, 0); + assert.equal(service.getEmptyWindowBackupPaths().length, 1); + + done(); + }); + + test('service validates backup workspaces on startup and cleans up (root workspaces)', done => { + + // 1) backup workspace path does not exist + service.registerWorkspaceBackupSync(toWorkspace(fooFile.fsPath)); + service.registerWorkspaceBackupSync(toWorkspace(barFile.fsPath)); + service.loadSync(); + assert.deepEqual(service.getWorkspaceBackups(), []); + + // 2) backup workspace path exists with empty contents within + fs.mkdirSync(service.toBackupPath(fooFile.fsPath)); + fs.mkdirSync(service.toBackupPath(barFile.fsPath)); + service.registerWorkspaceBackupSync(toWorkspace(fooFile.fsPath)); + service.registerWorkspaceBackupSync(toWorkspace(barFile.fsPath)); + service.loadSync(); + assert.deepEqual(service.getWorkspaceBackups(), []); + assert.ok(!fs.exists(service.toBackupPath(fooFile.fsPath))); + assert.ok(!fs.exists(service.toBackupPath(barFile.fsPath))); + + // 3) backup workspace path exists with empty folders within + fs.mkdirSync(service.toBackupPath(fooFile.fsPath)); + fs.mkdirSync(service.toBackupPath(barFile.fsPath)); + fs.mkdirSync(path.join(service.toBackupPath(fooFile.fsPath), 'file')); + fs.mkdirSync(path.join(service.toBackupPath(barFile.fsPath), 'untitled')); + service.registerWorkspaceBackupSync(toWorkspace(fooFile.fsPath)); + service.registerWorkspaceBackupSync(toWorkspace(barFile.fsPath)); + service.loadSync(); + assert.deepEqual(service.getWorkspaceBackups(), []); + assert.ok(!fs.exists(service.toBackupPath(fooFile.fsPath))); + assert.ok(!fs.exists(service.toBackupPath(barFile.fsPath))); + + // 4) backup workspace path points to a workspace that no longer exists + // so it should convert the backup worspace to an empty workspace backup + const fileBackups = path.join(service.toBackupPath(fooFile.fsPath), 'file'); + fs.mkdirSync(service.toBackupPath(fooFile.fsPath)); + fs.mkdirSync(service.toBackupPath(barFile.fsPath)); + fs.mkdirSync(fileBackups); + service.registerWorkspaceBackupSync(toWorkspace(fooFile.fsPath)); + assert.equal(service.getWorkspaceBackups().length, 1); + assert.equal(service.getEmptyWindowBackupPaths().length, 0); + fs.writeFileSync(path.join(fileBackups, 'backup.txt'), ''); + service.loadSync(); + assert.equal(service.getWorkspaceBackups().length, 0); + assert.equal(service.getEmptyWindowBackupPaths().length, 1); done(); }); suite('loadSync', () => { - test('getWorkspaceBackupPaths() should return [] when workspaces.json doesn\'t exist', () => { - assert.deepEqual(service.getWorkspaceBackupPaths(), []); + test('getFolderBackupPaths() should return [] when workspaces.json doesn\'t exist', () => { + assert.deepEqual(service.getFolderBackupPaths(), []); }); - test('getWorkspaceBackupPaths() should return [] when workspaces.json is not properly formed JSON', () => { + test('getFolderBackupPaths() should return [] when workspaces.json is not properly formed JSON', () => { fs.writeFileSync(backupWorkspacesPath, ''); service.loadSync(); - assert.deepEqual(service.getWorkspaceBackupPaths(), []); + assert.deepEqual(service.getFolderBackupPaths(), []); fs.writeFileSync(backupWorkspacesPath, '{]'); service.loadSync(); - assert.deepEqual(service.getWorkspaceBackupPaths(), []); + assert.deepEqual(service.getFolderBackupPaths(), []); fs.writeFileSync(backupWorkspacesPath, 'foo'); service.loadSync(); - assert.deepEqual(service.getWorkspaceBackupPaths(), []); + assert.deepEqual(service.getFolderBackupPaths(), []); }); - test('getWorkspaceBackupPaths() should return [] when folderWorkspaces in workspaces.json is absent', () => { + test('getFolderBackupPaths() should return [] when folderWorkspaces in workspaces.json is absent', () => { fs.writeFileSync(backupWorkspacesPath, '{}'); service.loadSync(); - assert.deepEqual(service.getWorkspaceBackupPaths(), []); + assert.deepEqual(service.getFolderBackupPaths(), []); }); - test('getWorkspaceBackupPaths() should return [] when folderWorkspaces in workspaces.json is not a string array', () => { + test('getFolderBackupPaths() should return [] when folderWorkspaces in workspaces.json is not a string array', () => { fs.writeFileSync(backupWorkspacesPath, '{"folderWorkspaces":{}}'); service.loadSync(); - assert.deepEqual(service.getWorkspaceBackupPaths(), []); + assert.deepEqual(service.getFolderBackupPaths(), []); fs.writeFileSync(backupWorkspacesPath, '{"folderWorkspaces":{"foo": ["bar"]}}'); service.loadSync(); - assert.deepEqual(service.getWorkspaceBackupPaths(), []); + assert.deepEqual(service.getFolderBackupPaths(), []); fs.writeFileSync(backupWorkspacesPath, '{"folderWorkspaces":{"foo": []}}'); service.loadSync(); - assert.deepEqual(service.getWorkspaceBackupPaths(), []); + assert.deepEqual(service.getFolderBackupPaths(), []); fs.writeFileSync(backupWorkspacesPath, '{"folderWorkspaces":{"foo": "bar"}}'); service.loadSync(); - assert.deepEqual(service.getWorkspaceBackupPaths(), []); + assert.deepEqual(service.getFolderBackupPaths(), []); fs.writeFileSync(backupWorkspacesPath, '{"folderWorkspaces":"foo"}'); service.loadSync(); - assert.deepEqual(service.getWorkspaceBackupPaths(), []); + assert.deepEqual(service.getFolderBackupPaths(), []); fs.writeFileSync(backupWorkspacesPath, '{"folderWorkspaces":1}'); service.loadSync(); - assert.deepEqual(service.getWorkspaceBackupPaths(), []); + assert.deepEqual(service.getFolderBackupPaths(), []); }); - test('getWorkspaceBackupPaths() should return [] when files.hotExit = "onExitAndWindowClose"', () => { - service.registerWindowForBackupsSync(1, false, null, fooFile.fsPath.toUpperCase()); - assert.deepEqual(service.getWorkspaceBackupPaths(), [fooFile.fsPath.toUpperCase()]); + test('getFolderBackupPaths() should return [] when files.hotExit = "onExitAndWindowClose"', () => { + service.registerFolderBackupSync(fooFile.fsPath.toUpperCase()); + assert.deepEqual(service.getFolderBackupPaths(), [fooFile.fsPath.toUpperCase()]); configService.setUserConfiguration('files.hotExit', HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE); service.loadSync(); - assert.deepEqual(service.getWorkspaceBackupPaths(), []); + assert.deepEqual(service.getFolderBackupPaths(), []); + }); + + test('getWorkspaceBackups() should return [] when workspaces.json doesn\'t exist', () => { + assert.deepEqual(service.getWorkspaceBackups(), []); + }); + + test('getWorkspaceBackups() should return [] when workspaces.json is not properly formed JSON', () => { + fs.writeFileSync(backupWorkspacesPath, ''); + service.loadSync(); + assert.deepEqual(service.getWorkspaceBackups(), []); + fs.writeFileSync(backupWorkspacesPath, '{]'); + service.loadSync(); + assert.deepEqual(service.getWorkspaceBackups(), []); + fs.writeFileSync(backupWorkspacesPath, 'foo'); + service.loadSync(); + assert.deepEqual(service.getWorkspaceBackups(), []); + }); + + test('getWorkspaceBackups() should return [] when folderWorkspaces in workspaces.json is absent', () => { + fs.writeFileSync(backupWorkspacesPath, '{}'); + service.loadSync(); + assert.deepEqual(service.getWorkspaceBackups(), []); + }); + + test('getWorkspaceBackups() should return [] when rootWorkspaces in workspaces.json is not a object array', () => { + fs.writeFileSync(backupWorkspacesPath, '{"rootWorkspaces":{}}'); + service.loadSync(); + assert.deepEqual(service.getWorkspaceBackups(), []); + fs.writeFileSync(backupWorkspacesPath, '{"rootWorkspaces":{"foo": ["bar"]}}'); + service.loadSync(); + assert.deepEqual(service.getWorkspaceBackups(), []); + fs.writeFileSync(backupWorkspacesPath, '{"rootWorkspaces":{"foo": []}}'); + service.loadSync(); + assert.deepEqual(service.getWorkspaceBackups(), []); + fs.writeFileSync(backupWorkspacesPath, '{"rootWorkspaces":{"foo": "bar"}}'); + service.loadSync(); + assert.deepEqual(service.getWorkspaceBackups(), []); + fs.writeFileSync(backupWorkspacesPath, '{"rootWorkspaces":"foo"}'); + service.loadSync(); + assert.deepEqual(service.getWorkspaceBackups(), []); + fs.writeFileSync(backupWorkspacesPath, '{"rootWorkspaces":1}'); + service.loadSync(); + assert.deepEqual(service.getWorkspaceBackups(), []); + }); + + test('getWorkspaceBackups() should return [] when files.hotExit = "onExitAndWindowClose"', () => { + service.registerWorkspaceBackupSync(toWorkspace(fooFile.fsPath.toUpperCase())); + assert.equal(service.getWorkspaceBackups().length, 1); + assert.deepEqual(service.getWorkspaceBackups().map(r => r.configPath), [fooFile.fsPath.toUpperCase()]); + configService.setUserConfiguration('files.hotExit', HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE); + service.loadSync(); + assert.deepEqual(service.getWorkspaceBackups(), []); }); test('getEmptyWorkspaceBackupPaths() should return [] when workspaces.json doesn\'t exist', () => { - assert.deepEqual(service.getEmptyWorkspaceBackupPaths(), []); + assert.deepEqual(service.getEmptyWindowBackupPaths(), []); }); test('getEmptyWorkspaceBackupPaths() should return [] when workspaces.json is not properly formed JSON', () => { fs.writeFileSync(backupWorkspacesPath, ''); service.loadSync(); - assert.deepEqual(service.getEmptyWorkspaceBackupPaths(), []); + assert.deepEqual(service.getEmptyWindowBackupPaths(), []); fs.writeFileSync(backupWorkspacesPath, '{]'); service.loadSync(); - assert.deepEqual(service.getEmptyWorkspaceBackupPaths(), []); + assert.deepEqual(service.getEmptyWindowBackupPaths(), []); fs.writeFileSync(backupWorkspacesPath, 'foo'); service.loadSync(); - assert.deepEqual(service.getEmptyWorkspaceBackupPaths(), []); + assert.deepEqual(service.getEmptyWindowBackupPaths(), []); }); test('getEmptyWorkspaceBackupPaths() should return [] when folderWorkspaces in workspaces.json is absent', () => { fs.writeFileSync(backupWorkspacesPath, '{}'); service.loadSync(); - assert.deepEqual(service.getEmptyWorkspaceBackupPaths(), []); + assert.deepEqual(service.getEmptyWindowBackupPaths(), []); }); test('getEmptyWorkspaceBackupPaths() should return [] when folderWorkspaces in workspaces.json is not a string array', () => { fs.writeFileSync(backupWorkspacesPath, '{"emptyWorkspaces":{}}'); service.loadSync(); - assert.deepEqual(service.getEmptyWorkspaceBackupPaths(), []); + assert.deepEqual(service.getEmptyWindowBackupPaths(), []); fs.writeFileSync(backupWorkspacesPath, '{"emptyWorkspaces":{"foo": ["bar"]}}'); service.loadSync(); - assert.deepEqual(service.getEmptyWorkspaceBackupPaths(), []); + assert.deepEqual(service.getEmptyWindowBackupPaths(), []); fs.writeFileSync(backupWorkspacesPath, '{"emptyWorkspaces":{"foo": []}}'); service.loadSync(); - assert.deepEqual(service.getEmptyWorkspaceBackupPaths(), []); + assert.deepEqual(service.getEmptyWindowBackupPaths(), []); fs.writeFileSync(backupWorkspacesPath, '{"emptyWorkspaces":{"foo": "bar"}}'); service.loadSync(); - assert.deepEqual(service.getEmptyWorkspaceBackupPaths(), []); + assert.deepEqual(service.getEmptyWindowBackupPaths(), []); fs.writeFileSync(backupWorkspacesPath, '{"emptyWorkspaces":"foo"}'); service.loadSync(); - assert.deepEqual(service.getEmptyWorkspaceBackupPaths(), []); + assert.deepEqual(service.getEmptyWindowBackupPaths(), []); fs.writeFileSync(backupWorkspacesPath, '{"emptyWorkspaces":1}'); service.loadSync(); - assert.deepEqual(service.getEmptyWorkspaceBackupPaths(), []); + assert.deepEqual(service.getEmptyWindowBackupPaths(), []); }); }); suite('dedupeFolderWorkspaces', () => { - test('should ignore duplicates on Windows and Mac', () => { + test('should ignore duplicates on Windows and Mac (folder workspace)', () => { // Skip test on Linux if (platform.isLinux) { return; } const backups: IBackupWorkspacesFormat = { + rootWorkspaces: [], folderWorkspaces: platform.isWindows ? ['c:\\FOO', 'C:\\FOO', 'c:\\foo'] : ['/FOO', '/foo'], emptyWorkspaces: [] }; - service.dedupeFolderWorkspaces(backups); + service.dedupeBackups(backups); assert.equal(backups.folderWorkspaces.length, 1); if (platform.isWindows) { @@ -243,13 +362,35 @@ suite('BackupMainService', () => { assert.deepEqual(backups.folderWorkspaces, ['/FOO'], 'should return the first duplicated entry'); } }); + + test('should ignore duplicates on Windows and Mac (root workspace)', () => { + // Skip test on Linux + if (platform.isLinux) { + return; + } + + const backups: IBackupWorkspacesFormat = { + rootWorkspaces: platform.isWindows ? [toWorkspace('c:\\FOO'), toWorkspace('C:\\FOO'), toWorkspace('c:\\foo')] : [toWorkspace('/FOO'), toWorkspace('/foo')], + folderWorkspaces: [], + emptyWorkspaces: [] + }; + + service.dedupeBackups(backups); + + assert.equal(backups.rootWorkspaces.length, 1); + if (platform.isWindows) { + assert.deepEqual(backups.rootWorkspaces.map(r => r.configPath), ['c:\\FOO'], 'should return the first duplicated entry'); + } else { + assert.deepEqual(backups.rootWorkspaces.map(r => r.configPath), ['/FOO'], 'should return the first duplicated entry'); + } + }); }); suite('registerWindowForBackups', () => { - test('should persist paths to workspaces.json', done => { - service.registerWindowForBackupsSync(1, false, null, fooFile.fsPath); - service.registerWindowForBackupsSync(2, false, null, barFile.fsPath); - assert.deepEqual(service.getWorkspaceBackupPaths(), [fooFile.fsPath, barFile.fsPath]); + test('should persist paths to workspaces.json (folder workspace)', done => { + service.registerFolderBackupSync(fooFile.fsPath); + service.registerFolderBackupSync(barFile.fsPath); + assert.deepEqual(service.getFolderBackupPaths(), [fooFile.fsPath, barFile.fsPath]); pfs.readFile(backupWorkspacesPath, 'utf-8').then(buffer => { const json = JSON.parse(buffer); assert.deepEqual(json.folderWorkspaces, [fooFile.fsPath, barFile.fsPath]); @@ -257,26 +398,57 @@ suite('BackupMainService', () => { }); }); - test('should always store the workspace path in workspaces.json using the case given, regardless of whether the file system is case-sensitive', done => { - service.registerWindowForBackupsSync(1, false, null, fooFile.fsPath.toUpperCase()); - assert.deepEqual(service.getWorkspaceBackupPaths(), [fooFile.fsPath.toUpperCase()]); + test('should persist paths to workspaces.json (root workspace)', done => { + const ws1 = toWorkspace(fooFile.fsPath); + service.registerWorkspaceBackupSync(ws1); + const ws2 = toWorkspace(barFile.fsPath); + service.registerWorkspaceBackupSync(ws2); + + assert.deepEqual(service.getWorkspaceBackups().map(b => b.configPath), [fooFile.fsPath, barFile.fsPath]); + assert.equal(ws1.id, service.getWorkspaceBackups()[0].id); + assert.equal(ws2.id, service.getWorkspaceBackups()[1].id); + + pfs.readFile(backupWorkspacesPath, 'utf-8').then(buffer => { + const json = JSON.parse(buffer); + + assert.deepEqual(json.rootWorkspaces.map(b => b.configPath), [fooFile.fsPath, barFile.fsPath]); + assert.equal(ws1.id, json.rootWorkspaces[0].id); + assert.equal(ws2.id, json.rootWorkspaces[1].id); + + done(); + }); + }); + + test('should always store the workspace path in workspaces.json using the case given, regardless of whether the file system is case-sensitive (folder workspace)', done => { + service.registerFolderBackupSync(fooFile.fsPath.toUpperCase()); + assert.deepEqual(service.getFolderBackupPaths(), [fooFile.fsPath.toUpperCase()]); pfs.readFile(backupWorkspacesPath, 'utf-8').then(buffer => { const json = JSON.parse(buffer); assert.deepEqual(json.folderWorkspaces, [fooFile.fsPath.toUpperCase()]); done(); }); }); + + test('should always store the workspace path in workspaces.json using the case given, regardless of whether the file system is case-sensitive (root workspace)', done => { + service.registerWorkspaceBackupSync(toWorkspace(fooFile.fsPath.toUpperCase())); + assert.deepEqual(service.getWorkspaceBackups().map(b => b.configPath), [fooFile.fsPath.toUpperCase()]); + pfs.readFile(backupWorkspacesPath, 'utf-8').then(buffer => { + const json = JSON.parse(buffer); + assert.deepEqual(json.rootWorkspaces.map(b => b.configPath), [fooFile.fsPath.toUpperCase()]); + done(); + }); + }); }); suite('removeBackupPathSync', () => { - test('should remove folder workspaces from workspaces.json', done => { - service.registerWindowForBackupsSync(1, false, null, fooFile.fsPath); - service.registerWindowForBackupsSync(2, false, null, barFile.fsPath); - service.removeBackupPathSync(fooFile.fsPath, false); + test('should remove folder workspaces from workspaces.json (folder workspace)', done => { + service.registerFolderBackupSync(fooFile.fsPath); + service.registerFolderBackupSync(barFile.fsPath); + service.removeBackupPathSync(fooFile.fsPath, service.backupsData.folderWorkspaces); pfs.readFile(backupWorkspacesPath, 'utf-8').then(buffer => { const json = JSON.parse(buffer); assert.deepEqual(json.folderWorkspaces, [barFile.fsPath]); - service.removeBackupPathSync(barFile.fsPath, false); + service.removeBackupPathSync(barFile.fsPath, service.backupsData.folderWorkspaces); pfs.readFile(backupWorkspacesPath, 'utf-8').then(content => { const json2 = JSON.parse(content); assert.deepEqual(json2.folderWorkspaces, []); @@ -285,14 +457,32 @@ suite('BackupMainService', () => { }); }); + test('should remove folder workspaces from workspaces.json (root workspace)', done => { + const ws1 = toWorkspace(fooFile.fsPath); + service.registerWorkspaceBackupSync(ws1); + const ws2 = toWorkspace(barFile.fsPath); + service.registerWorkspaceBackupSync(ws2); + service.removeBackupPathSync(ws1, service.backupsData.rootWorkspaces); + pfs.readFile(backupWorkspacesPath, 'utf-8').then(buffer => { + const json = JSON.parse(buffer); + assert.deepEqual(json.rootWorkspaces.map(r => r.configPath), [barFile.fsPath]); + service.removeBackupPathSync(ws2, service.backupsData.rootWorkspaces); + pfs.readFile(backupWorkspacesPath, 'utf-8').then(content => { + const json2 = JSON.parse(content); + assert.deepEqual(json2.rootWorkspaces, []); + done(); + }); + }); + }); + test('should remove empty workspaces from workspaces.json', done => { - service.registerWindowForBackupsSync(1, true, 'foo'); - service.registerWindowForBackupsSync(2, true, 'bar'); - service.removeBackupPathSync('foo', true); + service.registerEmptyWindowBackupSync('foo'); + service.registerEmptyWindowBackupSync('bar'); + service.removeBackupPathSync('foo', service.backupsData.emptyWorkspaces); pfs.readFile(backupWorkspacesPath, 'utf-8').then(buffer => { const json = JSON.parse(buffer); assert.deepEqual(json.emptyWorkspaces, ['bar']); - service.removeBackupPathSync('bar', true); + service.removeBackupPathSync('bar', service.backupsData.emptyWorkspaces); pfs.readFile(backupWorkspacesPath, 'utf-8').then(content => { const json2 = JSON.parse(content); assert.deepEqual(json2.emptyWorkspaces, []); @@ -302,10 +492,10 @@ suite('BackupMainService', () => { }); test('should fail gracefully when removing a path that doesn\'t exist', done => { - const workspacesJson: IBackupWorkspacesFormat = { folderWorkspaces: [fooFile.fsPath], emptyWorkspaces: [] }; + const workspacesJson: IBackupWorkspacesFormat = { rootWorkspaces: [], folderWorkspaces: [fooFile.fsPath], emptyWorkspaces: [] }; pfs.writeFile(backupWorkspacesPath, JSON.stringify(workspacesJson)).then(() => { - service.removeBackupPathSync(barFile.fsPath, false); - service.removeBackupPathSync('test', true); + service.removeBackupPathSync(barFile.fsPath, service.backupsData.folderWorkspaces); + service.removeBackupPathSync('test', service.backupsData.emptyWorkspaces); pfs.readFile(backupWorkspacesPath, 'utf-8').then(content => { const json = JSON.parse(content); assert.deepEqual(json.folderWorkspaces, [fooFile.fsPath]); @@ -317,7 +507,7 @@ suite('BackupMainService', () => { suite('getWorkspaceHash', () => { test('should perform an md5 hash on the path', () => { - assert.equal(service.getWorkspaceHash('/foo'), '1effb2475fcfba4f9e8b8a1dbc8f3caf'); + assert.equal(service.getFolderHash('/foo'), '1effb2475fcfba4f9e8b8a1dbc8f3caf'); }); test('should ignore case on Windows and Mac', () => { @@ -327,44 +517,57 @@ suite('BackupMainService', () => { } if (platform.isMacintosh) { - assert.equal(service.getWorkspaceHash('/foo'), service.getWorkspaceHash('/FOO')); + assert.equal(service.getFolderHash('/foo'), service.getFolderHash('/FOO')); } if (platform.isWindows) { - assert.equal(service.getWorkspaceHash('c:\\foo'), service.getWorkspaceHash('C:\\FOO')); + assert.equal(service.getFolderHash('c:\\foo'), service.getFolderHash('C:\\FOO')); } }); }); suite('mixed path casing', () => { - test('should handle case insensitive paths properly (registerWindowForBackupsSync)', done => { - service.registerWindowForBackupsSync(1, false, null, fooFile.fsPath); - service.registerWindowForBackupsSync(1, false, null, fooFile.fsPath.toUpperCase()); + test('should handle case insensitive paths properly (registerWindowForBackupsSync) (folder workspace)', done => { + service.registerFolderBackupSync(fooFile.fsPath); + service.registerFolderBackupSync(fooFile.fsPath.toUpperCase()); if (platform.isLinux) { - assert.equal(service.getWorkspaceBackupPaths().length, 2); + assert.equal(service.getFolderBackupPaths().length, 2); } else { - assert.equal(service.getWorkspaceBackupPaths().length, 1); + assert.equal(service.getFolderBackupPaths().length, 1); } done(); }); - test('should handle case insensitive paths properly (removeBackupPathSync)', done => { - - // same case - service.registerWindowForBackupsSync(1, false, null, fooFile.fsPath); - service.removeBackupPathSync(fooFile.fsPath, false); - assert.equal(service.getWorkspaceBackupPaths().length, 0); - - // mixed case - service.registerWindowForBackupsSync(1, false, null, fooFile.fsPath); - service.removeBackupPathSync(fooFile.fsPath.toUpperCase(), false); + test('should handle case insensitive paths properly (registerWindowForBackupsSync) (root workspace)', done => { + service.registerWorkspaceBackupSync(toWorkspace(fooFile.fsPath)); + service.registerWorkspaceBackupSync(toWorkspace(fooFile.fsPath.toUpperCase())); if (platform.isLinux) { - assert.equal(service.getWorkspaceBackupPaths().length, 1); + assert.equal(service.getWorkspaceBackups().length, 2); } else { - assert.equal(service.getWorkspaceBackupPaths().length, 0); + assert.equal(service.getWorkspaceBackups().length, 1); + } + + done(); + }); + + test('should handle case insensitive paths properly (removeBackupPathSync) (folder workspace)', done => { + + // same case + service.registerFolderBackupSync(fooFile.fsPath); + service.removeBackupPathSync(fooFile.fsPath, service.backupsData.folderWorkspaces); + assert.equal(service.getFolderBackupPaths().length, 0); + + // mixed case + service.registerFolderBackupSync(fooFile.fsPath); + service.removeBackupPathSync(fooFile.fsPath.toUpperCase(), service.backupsData.folderWorkspaces); + + if (platform.isLinux) { + assert.equal(service.getFolderBackupPaths().length, 1); + } else { + assert.equal(service.getFolderBackupPaths().length, 0); } done(); diff --git a/src/vs/platform/broadcast/electron-browser/broadcastService.ts b/src/vs/platform/broadcast/electron-browser/broadcastService.ts index 17696c41031..1b9e40df61b 100644 --- a/src/vs/platform/broadcast/electron-browser/broadcastService.ts +++ b/src/vs/platform/broadcast/electron-browser/broadcastService.ts @@ -9,6 +9,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' import Event, { Emitter } from 'vs/base/common/event'; import { ipcRenderer as ipc } from 'electron'; +import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier } from "vs/platform/workspaces/common/workspaces"; export const IBroadcastService = createDecorator('broadcastService'); @@ -20,7 +21,7 @@ export interface IBroadcast { export interface IBroadcastService { _serviceBrand: any; - broadcast(b: IBroadcast, target?: string): void; + broadcast(b: IBroadcast, target?: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier): void; onBroadcast: Event; } @@ -46,7 +47,7 @@ export class BroadcastService implements IBroadcastService { return this._onBroadcast.event; } - public broadcast(b: IBroadcast, target?: string): void { + public broadcast(b: IBroadcast, target?: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier): void { ipc.send('vscode:broadcast', this.windowId, target, { channel: b.channel, payload: b.payload diff --git a/src/vs/platform/environment/common/environment.ts b/src/vs/platform/environment/common/environment.ts index 679643e9353..a7fb3ba9d75 100644 --- a/src/vs/platform/environment/common/environment.ts +++ b/src/vs/platform/environment/common/environment.ts @@ -63,6 +63,8 @@ export interface IEnvironmentService { backupHome: string; backupWorkspacesPath: string; + workspacesHome: string; + isExtensionDevelopment: boolean; disableExtensions: boolean; extensionsPath: string; diff --git a/src/vs/platform/environment/node/environmentService.ts b/src/vs/platform/environment/node/environmentService.ts index 2a93543b82f..b149660ff4a 100644 --- a/src/vs/platform/environment/node/environmentService.ts +++ b/src/vs/platform/environment/node/environmentService.ts @@ -92,6 +92,9 @@ export class EnvironmentService implements IEnvironmentService { @memoize get backupWorkspacesPath(): string { return path.join(this.backupHome, 'workspaces.json'); } + @memoize + get workspacesHome(): string { return path.join(this.userDataPath, 'Workspaces'); } + @memoize get extensionsPath(): string { return parsePathArg(this._args['extensions-dir'], process) || path.join(this.userHome, product.dataFolderName, 'extensions'); } diff --git a/src/vs/platform/files/common/files.ts b/src/vs/platform/files/common/files.ts index 800aa7ed042..ef55c9c2171 100644 --- a/src/vs/platform/files/common/files.ts +++ b/src/vs/platform/files/common/files.ts @@ -524,7 +524,7 @@ export enum FileOperationResult { const WIN32_MAX_FILE_SIZE = 300 * 1024 * 1024; // 300 MB const GENERAL_MAX_FILE_SIZE = 16 * 1024 * 1024 * 1024; // 16 GB -export const MAX_FILE_SIZE = (process.arch === 'ia32' ? WIN32_MAX_FILE_SIZE : GENERAL_MAX_FILE_SIZE); +export const MAX_FILE_SIZE = (typeof process === 'object' ? (process.arch === 'ia32' ? WIN32_MAX_FILE_SIZE : GENERAL_MAX_FILE_SIZE) : WIN32_MAX_FILE_SIZE); export const AutoSaveConfiguration = { OFF: 'off', diff --git a/src/vs/platform/history/common/history.ts b/src/vs/platform/history/common/history.ts new file mode 100644 index 00000000000..0be4cd0e2b6 --- /dev/null +++ b/src/vs/platform/history/common/history.ts @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { IPath } from 'vs/platform/windows/common/windows'; +import CommonEvent from 'vs/base/common/event'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier } from "vs/platform/workspaces/common/workspaces"; + +export const IHistoryMainService = createDecorator('historyMainService'); + +export interface IRecentlyOpened { + workspaces: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier)[]; + files: string[]; +} + +export interface IHistoryMainService { + _serviceBrand: any; + + onRecentlyOpenedChange: CommonEvent; + + addRecentlyOpened(workspaces: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier)[], files: string[]): void; + + getRecentlyOpened(currentWorkspace?: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier, currentFiles?: IPath[]): IRecentlyOpened; + + removeFromRecentlyOpened(toRemove: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier): void; + removeFromRecentlyOpened(toRemove: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier)[]): void; + + clearRecentlyOpened(): void; + + updateWindowsJumpList(): void; +} \ No newline at end of file diff --git a/src/vs/platform/history/electron-main/historyMainService.ts b/src/vs/platform/history/electron-main/historyMainService.ts index 0cd122c6692..43c4f8d9bb9 100644 --- a/src/vs/platform/history/electron-main/historyMainService.ts +++ b/src/vs/platform/history/electron-main/historyMainService.ts @@ -15,150 +15,161 @@ import { ILogService } from 'vs/platform/log/common/log'; import { getPathLabel } from 'vs/base/common/labels'; import { IPath } from 'vs/platform/windows/common/windows'; import CommonEvent, { Emitter } from 'vs/base/common/event'; -import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { isWindows, isMacintosh, isLinux } from 'vs/base/common/platform'; +import { IWorkspaceIdentifier, IWorkspacesMainService, getWorkspaceLabel, ISingleFolderWorkspaceIdentifier } from "vs/platform/workspaces/common/workspaces"; +import { IHistoryMainService, IRecentlyOpened } from "vs/platform/history/common/history"; +import { IEnvironmentService } from "vs/platform/environment/common/environment"; -export const IHistoryMainService = createDecorator('historyMainService'); - -export interface IRecentPathsList { - folders: string[]; - files: string[]; -} - -export interface IHistoryMainService { - _serviceBrand: any; - - // events - onRecentPathsChange: CommonEvent; - - // methods - - addToRecentPathsList(paths: { path: string; isFile?: boolean; }[]): void; - getRecentPathsList(workspacePath?: string, filesToOpen?: IPath[]): IRecentPathsList; - removeFromRecentPathsList(path: string): void; - removeFromRecentPathsList(paths: string[]): void; - clearRecentPathsList(): void; - updateWindowsJumpList(): void; +export interface ILegacyRecentlyOpened extends IRecentlyOpened { + folders: string[]; // TODO@Ben migration } export class HistoryMainService implements IHistoryMainService { private static MAX_TOTAL_RECENT_ENTRIES = 100; - private static recentPathsListStorageKey = 'openedPathsList'; + private static recentlyOpenedStorageKey = 'openedPathsList'; _serviceBrand: any; - private _onRecentPathsChange = new Emitter(); - onRecentPathsChange: CommonEvent = this._onRecentPathsChange.event; + private _onRecentlyOpenedChange = new Emitter(); + onRecentlyOpenedChange: CommonEvent = this._onRecentlyOpenedChange.event; constructor( @IStorageService private storageService: IStorageService, - @ILogService private logService: ILogService + @ILogService private logService: ILogService, + @IWorkspacesMainService private workspacesService: IWorkspacesMainService, + @IEnvironmentService private environmentService: IEnvironmentService ) { } - public addToRecentPathsList(paths: { path: string; isFile?: boolean; }[]): void { - if (!paths || !paths.length) { - return; - } + public addRecentlyOpened(workspaces: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier)[], files: string[]): void { + if ((workspaces && workspaces.length > 0) || (files && files.length > 0)) { + const mru = this.getRecentlyOpened(); - const mru = this.getRecentPathsList(); - paths.forEach(p => { - const { path, isFile } = p; + // Workspaces + workspaces.forEach(workspace => { + mru.workspaces.unshift(workspace); + mru.workspaces = arrays.distinct(mru.workspaces, workspace => this.isSingleFolderWorkspace(workspace) ? workspace : workspace.id); - if (isFile) { + // Add to recent documents unless the workspace is untitled (macOS only, Windows can show workspaces separately) + const isUntitledWorkspace = !this.isSingleFolderWorkspace(workspace) && this.workspacesService.isUntitledWorkspace(workspace); + if (isMacintosh && !isUntitledWorkspace) { + app.addRecentDocument(this.isSingleFolderWorkspace(workspace) ? workspace : workspace.configPath); + } + }); + + // Files + files.forEach((path) => { mru.files.unshift(path); - mru.files = arrays.distinct(mru.files, (f) => isLinux ? f : f.toLowerCase()); - } else { - mru.folders.unshift(path); - mru.folders = arrays.distinct(mru.folders, (f) => isLinux ? f : f.toLowerCase()); - } + mru.files = arrays.distinct(mru.files, f => isLinux ? f : f.toLowerCase()); + + // Add to recent documents (Windows/macOS only) + if (isMacintosh || isWindows) { + app.addRecentDocument(path); + } + }); // Make sure its bounded - mru.folders = mru.folders.slice(0, HistoryMainService.MAX_TOTAL_RECENT_ENTRIES); + mru.workspaces = mru.workspaces.slice(0, HistoryMainService.MAX_TOTAL_RECENT_ENTRIES); mru.files = mru.files.slice(0, HistoryMainService.MAX_TOTAL_RECENT_ENTRIES); - // Add to recent documents (Windows/macOS only) - if (isMacintosh || isWindows) { - app.addRecentDocument(path); - } - }); - - this.storageService.setItem(HistoryMainService.recentPathsListStorageKey, mru); - this._onRecentPathsChange.fire(); + this.storageService.setItem(HistoryMainService.recentlyOpenedStorageKey, mru); + this._onRecentlyOpenedChange.fire(); + } } - public removeFromRecentPathsList(path: string): void; - public removeFromRecentPathsList(paths: string[]): void; - public removeFromRecentPathsList(arg1: any): void { - let paths: string[]; + private isSingleFolderWorkspace(obj: any): obj is string { + return typeof obj === 'string'; + } + + public removeFromRecentlyOpened(toRemove: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier): void; + public removeFromRecentlyOpened(toRemove: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier)[]): void; + public removeFromRecentlyOpened(arg1: any): void { + let workspacesOrFilesToRemove: any[]; if (Array.isArray(arg1)) { - paths = arg1; + workspacesOrFilesToRemove = arg1; } else { - paths = [arg1]; + workspacesOrFilesToRemove = [arg1]; } - const mru = this.getRecentPathsList(); + const mru = this.getRecentlyOpened(); let update = false; - paths.forEach(path => { - let index = mru.files.indexOf(path); + workspacesOrFilesToRemove.forEach((workspaceOrFileToRemove: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier) => { + + // Remove workspace + let index = arrays.firstIndex(mru.workspaces, workspace => this.equals(workspace, workspaceOrFileToRemove)); if (index >= 0) { - mru.files.splice(index, 1); + mru.workspaces.splice(index, 1); update = true; } - index = mru.folders.indexOf(path); - if (index >= 0) { - mru.folders.splice(index, 1); - update = true; + // Remove file + if (typeof workspaceOrFileToRemove === 'string') { + let index = mru.files.indexOf(workspaceOrFileToRemove); + if (index >= 0) { + mru.files.splice(index, 1); + update = true; + } } }); if (update) { - this.storageService.setItem(HistoryMainService.recentPathsListStorageKey, mru); - this._onRecentPathsChange.fire(); + this.storageService.setItem(HistoryMainService.recentlyOpenedStorageKey, mru); + this._onRecentlyOpenedChange.fire(); } } - public clearRecentPathsList(): void { - this.storageService.setItem(HistoryMainService.recentPathsListStorageKey, { folders: [], files: [] }); + private equals(w1: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier, w2: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier): boolean { + if (w1 === w2) { + return true; + } + + if (typeof w1 === 'string' || typeof w2 === 'string') { + return false; + } + + return w1.id === w2.id; + } + + public clearRecentlyOpened(): void { + this.storageService.setItem(HistoryMainService.recentlyOpenedStorageKey, { workspaces: [], folders: [], files: [] }); app.clearRecentDocuments(); // Event - this._onRecentPathsChange.fire(); + this._onRecentlyOpenedChange.fire(); } - public getRecentPathsList(workspacePath?: string, filesToOpen?: IPath[]): IRecentPathsList { + public getRecentlyOpened(currentWorkspace?: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier, currentFiles?: IPath[]): IRecentlyOpened { + let workspaces: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier)[]; let files: string[]; - let folders: string[]; // Get from storage - const storedRecents = this.storageService.getItem(HistoryMainService.recentPathsListStorageKey); + const storedRecents = this.storageService.getItem(HistoryMainService.recentlyOpenedStorageKey) as ILegacyRecentlyOpened; if (storedRecents) { + workspaces = storedRecents.workspaces || storedRecents.folders || []; files = storedRecents.files || []; - folders = storedRecents.folders || []; } else { + workspaces = []; files = []; - folders = []; + } + + // Add current workspace to beginning if set + if (currentWorkspace) { + workspaces.unshift(currentWorkspace); } // Add currently files to open to the beginning if any - if (filesToOpen) { - files.unshift(...filesToOpen.map(f => f.filePath)); - } - - // Add current workspace path to beginning if set - if (workspacePath) { - folders.unshift(workspacePath); + if (currentFiles) { + files.unshift(...currentFiles.map(f => f.filePath)); } // Clear those dupes + workspaces = arrays.distinct(workspaces, workspace => this.isSingleFolderWorkspace(workspace) ? workspace : workspace.id); files = arrays.distinct(files); - folders = arrays.distinct(folders); - return { files, folders }; + return { workspaces, files }; } public updateWindowsJumpList(): void { @@ -184,26 +195,29 @@ export class HistoryMainService implements IHistoryMainService { ] }); - // Recent Folders - if (this.getRecentPathsList().folders.length > 0) { + // Recent Workspaces + if (this.getRecentlyOpened().workspaces.length > 0) { // The user might have meanwhile removed items from the jump list and we have to respect that // so we need to update our list of recent paths with the choice of the user to not add them again // Also: Windows will not show our custom category at all if there is any entry which was removed // by the user! See https://github.com/Microsoft/vscode/issues/15052 - this.removeFromRecentPathsList(app.getJumpListSettings().removedItems.map(r => trim(r.args, '"'))); + this.removeFromRecentlyOpened(app.getJumpListSettings().removedItems.map(r => trim(r.args, '"'))); // Add entries jumpList.push({ type: 'custom', - name: nls.localize('recentFolders', "Recent Folders"), - items: this.getRecentPathsList().folders.slice(0, 7 /* limit number of entries here */).map(folder => { + name: nls.localize('recentFolders', "Recent Workspaces"), + items: this.getRecentlyOpened().workspaces.slice(0, 7 /* limit number of entries here */).map(workspace => { + const title = this.isSingleFolderWorkspace(workspace) ? (path.basename(workspace) || workspace) : getWorkspaceLabel(this.environmentService, workspace); + const description = this.isSingleFolderWorkspace(workspace) ? nls.localize('folderDesc', "{0} {1}", path.basename(workspace), getPathLabel(path.dirname(workspace))) : nls.localize('codeWorkspace', "Code Workspace"); + return { type: 'task', - title: path.basename(folder) || folder, // use the base name to show shorter entries in the list - description: nls.localize('folderDesc', "{0} {1}", path.basename(folder), getPathLabel(path.dirname(folder))), + title, + description, program: process.execPath, - args: `"${folder}"`, // open folder (use quotes to support paths with whitespaces) + args: `"${this.isSingleFolderWorkspace(workspace) ? workspace : workspace.configPath}"`, // open folder (use quotes to support paths with whitespaces) iconPath: 'explorer.exe', // simulate folder icon iconIndex: 0 }; diff --git a/src/vs/platform/instantiation/common/instantiation.ts b/src/vs/platform/instantiation/common/instantiation.ts index 17eae95cce9..8d7c113e080 100644 --- a/src/vs/platform/instantiation/common/instantiation.ts +++ b/src/vs/platform/instantiation/common/instantiation.ts @@ -25,39 +25,39 @@ export namespace _util { // --- interfaces ------ export interface IConstructorSignature0 { - new (...services: { _serviceBrand: any; }[]): T; + new(...services: { _serviceBrand: any; }[]): T; } export interface IConstructorSignature1 { - new (first: A1, ...services: { _serviceBrand: any; }[]): T; + new(first: A1, ...services: { _serviceBrand: any; }[]): T; } export interface IConstructorSignature2 { - new (first: A1, second: A2, ...services: { _serviceBrand: any; }[]): T; + new(first: A1, second: A2, ...services: { _serviceBrand: any; }[]): T; } export interface IConstructorSignature3 { - new (first: A1, second: A2, third: A3, ...services: { _serviceBrand: any; }[]): T; + new(first: A1, second: A2, third: A3, ...services: { _serviceBrand: any; }[]): T; } export interface IConstructorSignature4 { - new (first: A1, second: A2, third: A3, fourth: A4, ...services: { _serviceBrand: any; }[]): T; + new(first: A1, second: A2, third: A3, fourth: A4, ...services: { _serviceBrand: any; }[]): T; } export interface IConstructorSignature5 { - new (first: A1, second: A2, third: A3, fourth: A4, fifth: A5, ...services: { _serviceBrand: any; }[]): T; + new(first: A1, second: A2, third: A3, fourth: A4, fifth: A5, ...services: { _serviceBrand: any; }[]): T; } export interface IConstructorSignature6 { - new (first: A1, second: A2, third: A3, fourth: A4, fifth: A5, sixth: A6, ...services: { _serviceBrand: any; }[]): T; + new(first: A1, second: A2, third: A3, fourth: A4, fifth: A5, sixth: A6, ...services: { _serviceBrand: any; }[]): T; } export interface IConstructorSignature7 { - new (first: A1, second: A2, third: A3, fourth: A4, fifth: A5, sixth: A6, seventh: A7, ...services: { _serviceBrand: any; }[]): T; + new(first: A1, second: A2, third: A3, fourth: A4, fifth: A5, sixth: A6, seventh: A7, ...services: { _serviceBrand: any; }[]): T; } export interface IConstructorSignature8 { - new (first: A1, second: A2, third: A3, fourth: A4, fifth: A5, sixth: A6, seventh: A7, eigth: A8, ...services: { _serviceBrand: any; }[]): T; + new(first: A1, second: A2, third: A3, fourth: A4, fifth: A5, sixth: A6, seventh: A7, eigth: A8, ...services: { _serviceBrand: any; }[]): T; } export interface ServicesAccessor { diff --git a/src/vs/platform/log/common/log.ts b/src/vs/platform/log/common/log.ts index 51427842fea..6860e33cf4e 100644 --- a/src/vs/platform/log/common/log.ts +++ b/src/vs/platform/log/common/log.ts @@ -14,6 +14,8 @@ export interface ILogService { _serviceBrand: any; log(...args: any[]): void; + warn(...args: any[]): void; + error(...args: any[]): void; } export class LogMainService implements ILogService { @@ -25,7 +27,15 @@ export class LogMainService implements ILogService { public log(...args: any[]): void { if (this.environmentService.verbose) { - console.log(`\x1b[93m[main ${new Date().toLocaleTimeString()}]\x1b[0m`, ...args); + console.log(`\x1b[90m[main ${new Date().toLocaleTimeString()}]\x1b[0m`, ...args); } } + + public error(...args: any[]): void { + console.error(`\x1b[91m[main ${new Date().toLocaleTimeString()}]\x1b[0m`, ...args); + } + + public warn(...args: any[]): void { + console.warn(`\x1b[93m[main ${new Date().toLocaleTimeString()}]\x1b[0m`, ...args); + } } \ No newline at end of file diff --git a/src/vs/platform/markers/common/problemMatcher.ts b/src/vs/platform/markers/common/problemMatcher.ts index d989ecc23f9..cf49ef05252 100644 --- a/src/vs/platform/markers/common/problemMatcher.ts +++ b/src/vs/platform/markers/common/problemMatcher.ts @@ -116,6 +116,7 @@ export interface ProblemMatcher { export interface NamedProblemMatcher extends ProblemMatcher { name: string; label: string; + deprecated?: boolean; } export interface NamedMultiLineProblemPattern { @@ -1586,6 +1587,7 @@ class ProblemMatcherRegistryImpl implements IProblemMatcherRegistry { this.add({ name: 'lessCompile', label: localize('lessCompile', 'Less problems'), + deprecated: true, owner: 'lessCompile', applyTo: ApplyToKind.allDocuments, fileLocation: FileLocationKind.Absolute, diff --git a/src/vs/platform/storage/common/storageService.ts b/src/vs/platform/storage/common/storageService.ts index 61bee17ba70..a5e2c7fa532 100644 --- a/src/vs/platform/storage/common/storageService.ts +++ b/src/vs/platform/storage/common/storageService.ts @@ -8,7 +8,6 @@ import types = require('vs/base/common/types'); import errors = require('vs/base/common/errors'); import strings = require('vs/base/common/strings'); import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; -import { IWorkspace } from 'vs/platform/workspace/common/workspace'; // Browser localStorage interface export interface IStorage { @@ -38,18 +37,18 @@ export class StorageService implements IStorageService { constructor( globalStorage: IStorage, workspaceStorage: IStorage, - workspace?: IWorkspace, + workspaceId?: string, legacyWorkspaceId?: number ) { this.globalStorage = globalStorage; this.workspaceStorage = workspaceStorage || globalStorage; // Calculate workspace storage key - this.workspaceKey = this.getWorkspaceKey(workspace ? workspace.id : void 0); + this.workspaceKey = this.getWorkspaceKey(workspaceId); // Make sure to delete all workspace storage if the workspace has been recreated meanwhile // which is only possible if a id property is provided that we can check on - if (workspace && types.isNumber(legacyWorkspaceId)) { + if (types.isNumber(legacyWorkspaceId)) { this.cleanupWorkspaceScope(legacyWorkspaceId); } } diff --git a/src/vs/platform/storage/test/common/storageService.test.ts b/src/vs/platform/storage/test/common/storageService.test.ts index c70b62ec900..42af372c759 100644 --- a/src/vs/platform/storage/test/common/storageService.test.ts +++ b/src/vs/platform/storage/test/common/storageService.test.ts @@ -29,7 +29,7 @@ suite('Workbench StorageSevice', () => { }); test('Swap Data with undefined default value', () => { - let s = new StorageService(new InMemoryLocalStorage(), null, contextService.getWorkspace()); + let s = new StorageService(new InMemoryLocalStorage(), null, contextService.getWorkspace().id); s.swap('Monaco.IDE.Core.Storage.Test.swap', 'foobar', 'barfoo'); assert.strictEqual('foobar', s.get('Monaco.IDE.Core.Storage.Test.swap')); @@ -40,7 +40,7 @@ suite('Workbench StorageSevice', () => { }); test('Remove Data', () => { - let s = new StorageService(new InMemoryLocalStorage(), null, contextService.getWorkspace()); + let s = new StorageService(new InMemoryLocalStorage(), null, contextService.getWorkspace().id); s.store('Monaco.IDE.Core.Storage.Test.remove', 'foobar'); assert.strictEqual('foobar', s.get('Monaco.IDE.Core.Storage.Test.remove')); @@ -49,7 +49,7 @@ suite('Workbench StorageSevice', () => { }); test('Get Data, Integer, Boolean', () => { - let s = new StorageService(new InMemoryLocalStorage(), null, contextService.getWorkspace()); + let s = new StorageService(new InMemoryLocalStorage(), null, contextService.getWorkspace().id); assert.strictEqual(s.get('Monaco.IDE.Core.Storage.Test.get', StorageScope.GLOBAL, 'foobar'), 'foobar'); assert.strictEqual(s.get('Monaco.IDE.Core.Storage.Test.get', StorageScope.GLOBAL, ''), ''); @@ -84,14 +84,14 @@ suite('Workbench StorageSevice', () => { test('StorageSevice cleans up when workspace changes', () => { let storageImpl = new InMemoryLocalStorage(); let time = new Date().getTime(); - let s = new StorageService(storageImpl, null, contextService.getWorkspace(), time); + let s = new StorageService(storageImpl, null, contextService.getWorkspace().id, time); s.store('key1', 'foobar'); s.store('key2', 'something'); s.store('wkey1', 'foo', StorageScope.WORKSPACE); s.store('wkey2', 'foo2', StorageScope.WORKSPACE); - s = new StorageService(storageImpl, null, contextService.getWorkspace(), time); + s = new StorageService(storageImpl, null, contextService.getWorkspace().id, time); assert.strictEqual(s.get('key1', StorageScope.GLOBAL), 'foobar'); assert.strictEqual(s.get('key1', StorageScope.WORKSPACE, null), null); @@ -100,7 +100,7 @@ suite('Workbench StorageSevice', () => { assert.strictEqual(s.get('wkey1', StorageScope.WORKSPACE), 'foo'); assert.strictEqual(s.get('wkey2', StorageScope.WORKSPACE), 'foo2'); - s = new StorageService(storageImpl, null, contextService.getWorkspace(), time + 100); + s = new StorageService(storageImpl, null, contextService.getWorkspace().id, time + 100); assert.strictEqual(s.get('key1', StorageScope.GLOBAL), 'foobar'); assert.strictEqual(s.get('key1', StorageScope.WORKSPACE, null), null); diff --git a/src/vs/platform/telemetry/test/electron-browser/commonProperties.test.ts b/src/vs/platform/telemetry/test/electron-browser/commonProperties.test.ts index a504489d833..810f50728c5 100644 --- a/src/vs/platform/telemetry/test/electron-browser/commonProperties.test.ts +++ b/src/vs/platform/telemetry/test/electron-browser/commonProperties.test.ts @@ -17,7 +17,7 @@ suite('Telemetry - common properties', function () { let storageService; setup(() => { - storageService = new StorageService(new InMemoryLocalStorage(), null, TestWorkspace); + storageService = new StorageService(new InMemoryLocalStorage(), null, TestWorkspace.id); }); test('default', function () { diff --git a/src/vs/platform/telemetry/test/electron-browser/telemetryService.test.ts b/src/vs/platform/telemetry/test/electron-browser/telemetryService.test.ts index d435adbd74d..6acd2f7754a 100644 --- a/src/vs/platform/telemetry/test/electron-browser/telemetryService.test.ts +++ b/src/vs/platform/telemetry/test/electron-browser/telemetryService.test.ts @@ -678,8 +678,8 @@ suite('TelemetryService', () => { _serviceBrand: undefined, getConfiguration() { return { - enableTelemetry - }; + enableTelemetry: enableTelemetry + } as any; }, getConfigurationData(): any { return null; diff --git a/src/vs/platform/windows/common/windows.ts b/src/vs/platform/windows/common/windows.ts index d80c2b18321..3b8035563d9 100644 --- a/src/vs/platform/windows/common/windows.ts +++ b/src/vs/platform/windows/common/windows.ts @@ -11,6 +11,8 @@ import Event from 'vs/base/common/event'; import { ITelemetryData } from 'vs/platform/telemetry/common/telemetry'; import { IProcessEnvironment } from 'vs/base/common/platform'; import { ParsedArgs } from 'vs/platform/environment/common/environment'; +import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier } from "vs/platform/workspaces/common/workspaces"; +import { IRecentlyOpened } from "vs/platform/history/common/history"; export const IWindowsService = createDecorator('windowsService'); @@ -28,13 +30,13 @@ export interface IWindowsService { reloadWindow(windowId: number): TPromise; openDevTools(windowId: number): TPromise; toggleDevTools(windowId: number): TPromise; - closeFolder(windowId: number): TPromise; + closeWorkspace(windowId: number): TPromise; toggleFullScreen(windowId: number): TPromise; setRepresentedFilename(windowId: number, fileName: string): TPromise; - addToRecentlyOpen(paths: { path: string, isFile?: boolean }[]): TPromise; - removeFromRecentlyOpen(paths: string[]): TPromise; - clearRecentPathsList(): TPromise; - getRecentlyOpen(windowId: number): TPromise<{ files: string[]; folders: string[]; }>; + addRecentlyOpened(files: string[]): TPromise; + removeFromRecentlyOpened(toRemove: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier)[]): TPromise; + clearRecentlyOpened(): TPromise; + getRecentlyOpened(windowId: number): TPromise; focusWindow(windowId: number): TPromise; closeWindow(windowId: number): TPromise; isFocused(windowId: number): TPromise; @@ -54,7 +56,7 @@ export interface IWindowsService { openWindow(paths: string[], options?: { forceNewWindow?: boolean, forceReuseWindow?: boolean }): TPromise; openNewWindow(): TPromise; showWindow(windowId: number): TPromise; - getWindows(): TPromise<{ id: number; path: string; title: string; filename?: string; }[]>; + getWindows(): TPromise<{ id: number; workspace?: IWorkspaceIdentifier; folderPath?: string; title: string; filename?: string; }[]>; getWindowCount(): TPromise; log(severity: string, ...messages: string[]): TPromise; // TODO@joao: what? @@ -83,10 +85,10 @@ export interface IWindowService { reloadWindow(): TPromise; openDevTools(): TPromise; toggleDevTools(): TPromise; - closeFolder(): TPromise; + closeWorkspace(): TPromise; toggleFullScreen(): TPromise; setRepresentedFilename(fileName: string): TPromise; - getRecentlyOpen(): TPromise<{ files: string[]; folders: string[]; }>; + getRecentlyOpened(): TPromise; focusWindow(): TPromise; closeWindow(): TPromise; isFocused(): TPromise; @@ -97,11 +99,12 @@ export interface IWindowService { onWindowTitleDoubleClick(): TPromise; showMessageBox(options: Electron.ShowMessageBoxOptions): number; showSaveDialog(options: Electron.SaveDialogOptions, callback?: (fileName: string) => void): string; + showOpenDialog(options: Electron.OpenDialogOptions, callback?: (fileNames: string[]) => void): string[]; } export type MenuBarVisibility = 'default' | 'visible' | 'toggle' | 'hidden'; -export interface IWindowConfiguration { +export interface IWindowsConfiguration { window: IWindowSettings; } @@ -186,11 +189,17 @@ export interface IOpenFileRequest { export interface IWindowConfiguration extends ParsedArgs, IOpenFileRequest { appRoot: string; execPath: string; + isInitialStartup?: boolean; userEnv: IProcessEnvironment; + nodeCachedDataDir: string; + + backupPath?: string; + + workspace?: IWorkspaceIdentifier; + folderPath?: string; isISOKeyboard?: boolean; - zoomLevel?: number; fullscreen?: boolean; highContrast?: boolean; @@ -198,15 +207,7 @@ export interface IWindowConfiguration extends ParsedArgs, IOpenFileRequest { backgroundColor?: string; accessibilitySupport?: boolean; - isInitialStartup?: boolean; - perfStartTime?: number; perfAppReady?: number; perfWindowLoadTime?: number; - - workspacePath?: string; - - backupPath?: string; - - nodeCachedDataDir: string; } \ No newline at end of file diff --git a/src/vs/platform/windows/common/windowsIpc.ts b/src/vs/platform/windows/common/windowsIpc.ts index abbc1dc8e33..786ebc63c3e 100644 --- a/src/vs/platform/windows/common/windowsIpc.ts +++ b/src/vs/platform/windows/common/windowsIpc.ts @@ -10,6 +10,8 @@ import Event, { buffer } from 'vs/base/common/event'; import { IChannel, eventToCall, eventFromCall } from 'vs/base/parts/ipc/common/ipc'; import { IWindowsService } from './windows'; import { ITelemetryData } from 'vs/platform/telemetry/common/telemetry'; +import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier } from "vs/platform/workspaces/common/workspaces"; +import { IRecentlyOpened } from "vs/platform/history/common/history"; export interface IWindowsChannel extends IChannel { call(command: 'event:onWindowOpen'): TPromise; @@ -20,13 +22,13 @@ export interface IWindowsChannel extends IChannel { call(command: 'pickFolder', arg: [number, { buttonLabel: string; title: string; }]): TPromise; call(command: 'reloadWindow', arg: number): TPromise; call(command: 'toggleDevTools', arg: number): TPromise; - call(command: 'closeFolder', arg: number): TPromise; + call(command: 'closeWorkspace', arg: number): TPromise; call(command: 'toggleFullScreen', arg: number): TPromise; call(command: 'setRepresentedFilename', arg: [number, string]): TPromise; - call(command: 'addToRecentlyOpen', arg: { path: string, isFile?: boolean }[]): TPromise; - call(command: 'removeFromRecentlyOpen', arg: string[]): TPromise; - call(command: 'clearRecentPathsList'): TPromise; - call(command: 'getRecentlyOpen', arg: number): TPromise<{ files: string[]; folders: string[]; }>; + call(command: 'addRecentlyOpened', arg: string[]): TPromise; + call(command: 'removeFromRecentlyOpened', arg: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier)[]): TPromise; + call(command: 'clearRecentlyOpened'): TPromise; + call(command: 'getRecentlyOpened', arg: number): TPromise; call(command: 'focusWindow', arg: number): TPromise; call(command: 'closeWindow', arg: number): TPromise; call(command: 'isFocused', arg: number): TPromise; @@ -39,7 +41,7 @@ export interface IWindowsChannel extends IChannel { call(command: 'openWindow', arg: [string[], { forceNewWindow?: boolean, forceReuseWindow?: boolean }]): TPromise; call(command: 'openNewWindow'): TPromise; call(command: 'showWindow', arg: number): TPromise; - call(command: 'getWindows'): TPromise<{ id: number; path: string; title: string; }[]>; + call(command: 'getWindows'): TPromise<{ id: number; workspace?: IWorkspaceIdentifier; folderPath?: string; title: string; filename?: string; }[]>; call(command: 'getWindowCount'): TPromise; call(command: 'relaunch', arg: { addArgs?: string[], removeArgs?: string[] }): TPromise; call(command: 'whenSharedProcessReady'): TPromise; @@ -73,13 +75,13 @@ export class WindowsChannel implements IWindowsChannel { case 'reloadWindow': return this.service.reloadWindow(arg); case 'openDevTools': return this.service.openDevTools(arg); case 'toggleDevTools': return this.service.toggleDevTools(arg); - case 'closeFolder': return this.service.closeFolder(arg); + case 'closeWorkspace': return this.service.closeWorkspace(arg); case 'toggleFullScreen': return this.service.toggleFullScreen(arg); case 'setRepresentedFilename': return this.service.setRepresentedFilename(arg[0], arg[1]); - case 'addToRecentlyOpen': return this.service.addToRecentlyOpen(arg); - case 'removeFromRecentlyOpen': return this.service.removeFromRecentlyOpen(arg); - case 'clearRecentPathsList': return this.service.clearRecentPathsList(); - case 'getRecentlyOpen': return this.service.getRecentlyOpen(arg); + case 'addRecentlyOpened': return this.service.addRecentlyOpened(arg); + case 'removeFromRecentlyOpened': return this.service.removeFromRecentlyOpened(arg); + case 'clearRecentlyOpened': return this.service.clearRecentlyOpened(); + case 'getRecentlyOpened': return this.service.getRecentlyOpened(arg); case 'focusWindow': return this.service.focusWindow(arg); case 'closeWindow': return this.service.closeWindow(arg); case 'isFocused': return this.service.isFocused(arg); @@ -147,8 +149,8 @@ export class WindowsChannelClient implements IWindowsService { return this.channel.call('toggleDevTools', windowId); } - closeFolder(windowId: number): TPromise { - return this.channel.call('closeFolder', windowId); + closeWorkspace(windowId: number): TPromise { + return this.channel.call('closeWorkspace', windowId); } toggleFullScreen(windowId: number): TPromise { @@ -159,20 +161,20 @@ export class WindowsChannelClient implements IWindowsService { return this.channel.call('setRepresentedFilename', [windowId, fileName]); } - addToRecentlyOpen(paths: { path: string, isFile?: boolean }[]): TPromise { - return this.channel.call('addToRecentlyOpen', paths); + addRecentlyOpened(files: string[]): TPromise { + return this.channel.call('addRecentlyOpened', files); } - removeFromRecentlyOpen(paths: string[]): TPromise { - return this.channel.call('removeFromRecentlyOpen', paths); + removeFromRecentlyOpened(toRemove: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier)[]): TPromise { + return this.channel.call('removeFromRecentlyOpened', toRemove); } - clearRecentPathsList(): TPromise { - return this.channel.call('clearRecentPathsList'); + clearRecentlyOpened(): TPromise { + return this.channel.call('clearRecentlyOpened'); } - getRecentlyOpen(windowId: number): TPromise<{ files: string[]; folders: string[]; }> { - return this.channel.call('getRecentlyOpen', windowId); + getRecentlyOpened(windowId: number): TPromise { + return this.channel.call('getRecentlyOpened', windowId); } focusWindow(windowId: number): TPromise { @@ -235,7 +237,7 @@ export class WindowsChannelClient implements IWindowsService { return this.channel.call('showWindow', windowId); } - getWindows(): TPromise<{ id: number; path: string; title: string; }[]> { + getWindows(): TPromise<{ id: number; workspace?: IWorkspaceIdentifier; folderPath?: string; title: string; filename?: string; }[]> { return this.channel.call('getWindows'); } diff --git a/src/vs/platform/windows/electron-browser/windowService.ts b/src/vs/platform/windows/electron-browser/windowService.ts index c9eee55187e..731e318e9e1 100644 --- a/src/vs/platform/windows/electron-browser/windowService.ts +++ b/src/vs/platform/windows/electron-browser/windowService.ts @@ -9,6 +9,7 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { IWindowService, IWindowsService } from 'vs/platform/windows/common/windows'; import { ITelemetryData } from 'vs/platform/telemetry/common/telemetry'; import { remote } from 'electron'; +import { IRecentlyOpened } from "vs/platform/history/common/history"; export class WindowService implements IWindowService { @@ -51,8 +52,8 @@ export class WindowService implements IWindowService { return this.windowsService.toggleDevTools(this.windowId); } - closeFolder(): TPromise { - return this.windowsService.closeFolder(this.windowId); + closeWorkspace(): TPromise { + return this.windowsService.closeWorkspace(this.windowId); } closeWindow(): TPromise { @@ -67,8 +68,8 @@ export class WindowService implements IWindowService { return this.windowsService.setRepresentedFilename(this.windowId, fileName); } - getRecentlyOpen(): TPromise<{ files: string[]; folders: string[]; }> { - return this.windowsService.getRecentlyOpen(this.windowId); + getRecentlyOpened(): TPromise { + return this.windowsService.getRecentlyOpened(this.windowId); } focusWindow(): TPromise { @@ -110,4 +111,12 @@ export class WindowService implements IWindowService { return remote.dialog.showSaveDialog(remote.getCurrentWindow(), options); // https://github.com/electron/electron/issues/4936 } + + showOpenDialog(options: Electron.OpenDialogOptions, callback?: (fileNames: string[]) => void): string[] { + if (callback) { + return remote.dialog.showOpenDialog(remote.getCurrentWindow(), options, callback); + } + + return remote.dialog.showOpenDialog(remote.getCurrentWindow(), options); // https://github.com/electron/electron/issues/4936 + } } diff --git a/src/vs/platform/windows/electron-main/windows.ts b/src/vs/platform/windows/electron-main/windows.ts index 7faee2803d9..ed963df3d45 100644 --- a/src/vs/platform/windows/electron-main/windows.ts +++ b/src/vs/platform/windows/electron-main/windows.ts @@ -12,12 +12,16 @@ import Event from 'vs/base/common/event'; import { ITelemetryData } from 'vs/platform/telemetry/common/telemetry'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IProcessEnvironment } from 'vs/base/common/platform'; +import { IWorkspaceIdentifier } from "vs/platform/workspaces/common/workspaces"; export interface ICodeWindow { id: number; win: Electron.BrowserWindow; config: IWindowConfiguration; - openedWorkspacePath: string; + + openedFolderPath: string; + openedWorkspace: IWorkspaceIdentifier; + lastFocusTime: number; readyState: ReadyState; @@ -48,6 +52,7 @@ export interface IWindowsMainService { // methods ready(initialUserEnv: IProcessEnvironment): void; reload(win: ICodeWindow, cli?: ParsedArgs): void; + closeWorkspace(win: ICodeWindow): void; open(openConfig: IOpenConfiguration): ICodeWindow[]; openExtensionDevelopmentHostWindow(openConfig: IOpenConfiguration): void; pickFileFolderAndOpen(forceNewWindow?: boolean, data?: ITelemetryData): void; @@ -76,7 +81,6 @@ export interface IOpenConfiguration { forceNewWindow?: boolean; forceReuseWindow?: boolean; forceEmpty?: boolean; - windowToUse?: ICodeWindow; diffMode?: boolean; initialStartup?: boolean; } diff --git a/src/vs/platform/windows/electron-main/windowsService.ts b/src/vs/platform/windows/electron-main/windowsService.ts index 955d0b5fb49..8cc7e7e47f2 100644 --- a/src/vs/platform/windows/electron-main/windowsService.ts +++ b/src/vs/platform/windows/electron-main/windowsService.ts @@ -18,8 +18,9 @@ import { IURLService } from 'vs/platform/url/common/url'; import { ITelemetryData } from 'vs/platform/telemetry/common/telemetry'; import { ILifecycleService } from "vs/platform/lifecycle/electron-main/lifecycleMain"; import { IWindowsMainService, ISharedProcess } from "vs/platform/windows/electron-main/windows"; -import { IHistoryMainService } from "vs/platform/history/electron-main/historyMainService"; +import { IHistoryMainService, IRecentlyOpened } from "vs/platform/history/common/history"; import { findExtensionDevelopmentWindow } from "vs/code/node/windowsFinder"; +import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier } from "vs/platform/workspaces/common/workspaces"; export class WindowsService implements IWindowsService, IDisposable { @@ -109,11 +110,11 @@ export class WindowsService implements IWindowsService, IDisposable { return TPromise.as(null); } - closeFolder(windowId: number): TPromise { + closeWorkspace(windowId: number): TPromise { const codeWindow = this.windowsMainService.getWindowById(windowId); if (codeWindow) { - this.windowsMainService.open({ context: OpenContext.API, cli: this.environmentService.args, forceEmpty: true, windowToUse: codeWindow, forceReuseWindow: true }); + this.windowsMainService.closeWorkspace(codeWindow); } return TPromise.as(null); @@ -139,32 +140,34 @@ export class WindowsService implements IWindowsService, IDisposable { return TPromise.as(null); } - addToRecentlyOpen(paths: { path: string, isFile?: boolean }[]): TPromise { - this.historyService.addToRecentPathsList(paths); + addRecentlyOpened(files: string[]): TPromise { + this.historyService.addRecentlyOpened(void 0, files); return TPromise.as(null); } - removeFromRecentlyOpen(paths: string[]): TPromise { - this.historyService.removeFromRecentPathsList(paths); + removeFromRecentlyOpened(toRemove: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier)[]): TPromise { + this.historyService.removeFromRecentlyOpened(toRemove); return TPromise.as(null); } - clearRecentPathsList(): TPromise { - this.historyService.clearRecentPathsList(); + clearRecentlyOpened(): TPromise { + this.historyService.clearRecentlyOpened(); + return TPromise.as(null); } - getRecentlyOpen(windowId: number): TPromise<{ files: string[]; folders: string[]; }> { + getRecentlyOpened(windowId: number): TPromise { const codeWindow = this.windowsMainService.getWindowById(windowId); if (codeWindow) { - const { files, folders } = this.historyService.getRecentPathsList(codeWindow.config.workspacePath, codeWindow.config.filesToOpen); - return TPromise.as({ files, folders }); + const recentlyOpened = this.historyService.getRecentlyOpened(codeWindow.config.workspace || codeWindow.config.folderPath, codeWindow.config.filesToOpen); + + return TPromise.as(recentlyOpened); } - return TPromise.as({ files: [], folders: [] }); + return TPromise.as({ workspaces: [], files: [] }); } focusWindow(windowId: number): TPromise { @@ -271,9 +274,9 @@ export class WindowsService implements IWindowsService, IDisposable { return TPromise.as(null); } - getWindows(): TPromise<{ id: number; path: string; title: string; }[]> { + getWindows(): TPromise<{ id: number; workspace?: IWorkspaceIdentifier; folderPath?: string; title: string; filename?: string; }[]> { const windows = this.windowsMainService.getWindows(); - const result = windows.map(w => ({ path: w.openedWorkspacePath, title: w.win.getTitle(), id: w.id, filename: w.getRepresentedFilename() })); + const result = windows.map(w => ({ id: w.id, workspace: w.openedWorkspace, openedFolderPath: w.openedFolderPath, title: w.win.getTitle(), filename: w.getRepresentedFilename() })); return TPromise.as(result); } diff --git a/src/vs/platform/workspace/common/workspace.ts b/src/vs/platform/workspace/common/workspace.ts index f84cf1cfe9c..598f9e64a0a 100644 --- a/src/vs/platform/workspace/common/workspace.ts +++ b/src/vs/platform/workspace/common/workspace.ts @@ -21,6 +21,16 @@ export interface IWorkspaceContextService { */ hasWorkspace(): boolean; + /** + * Returns iff the application was opened with a folder. + */ + hasFolderWorkspace(): boolean; + + /** + * Returns iff the application was opened with a workspace that can have one or more folders. + */ + hasMultiFolderWorkspace(): boolean; + /** * Provides access to the workspace object the platform is running with. This may be null if the workbench was opened * without workspace (empty); @@ -84,9 +94,14 @@ export interface IWorkspace { readonly name: string; /** - * Mutliple roots in this workspace. First entry is master and never changes. + * Roots in the workspace. */ readonly roots: URI[]; + + /** + * the location of the workspace configuration + */ + readonly configuration?: URI; } export class LegacyWorkspace implements ILegacyWorkspace { @@ -140,7 +155,8 @@ export class Workspace implements IWorkspace { constructor( public readonly id: string, private _name: string, - private _roots: URI[] + private _roots: URI[], + public readonly configuration: URI = null ) { this.updateRootsMap(); } diff --git a/src/vs/platform/workspaces/common/workspaces.ts b/src/vs/platform/workspaces/common/workspaces.ts new file mode 100644 index 00000000000..1b98da22dff --- /dev/null +++ b/src/vs/platform/workspaces/common/workspaces.ts @@ -0,0 +1,56 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { isParent } from "vs/platform/files/common/files"; +import { localize } from "vs/nls"; +import { basename } from "vs/base/common/paths"; +import { isLinux } from "vs/base/common/platform"; +import { IEnvironmentService } from "vs/platform/environment/common/environment"; + +export const IWorkspacesMainService = createDecorator('workspacesMainService'); +export const IWorkspacesService = createDecorator('workspacesService'); + +export const WORKSPACE_EXTENSION = 'code-workspace'; + +/** + * A single folder workspace identifier is just the path to the folder. + */ +export type ISingleFolderWorkspaceIdentifier = string; + +export interface IWorkspaceIdentifier { + id: string; + configPath: string; +} + +export interface IStoredWorkspace { + id: string; + folders: string[]; +} + +export interface IWorkspacesMainService extends IWorkspacesService { + _serviceBrand: any; + + resolveWorkspaceSync(path: string): IWorkspaceIdentifier; + isUntitledWorkspace(workspace: IWorkspaceIdentifier): boolean; +} + +export interface IWorkspacesService { + _serviceBrand: any; + + createWorkspace(folders?: string[]): TPromise; + saveWorkspace(workspace: IWorkspaceIdentifier, target: string): TPromise; +} + +export function getWorkspaceLabel(environmentService: IEnvironmentService, workspace: IWorkspaceIdentifier): string { + if (isParent(workspace.configPath, environmentService.workspacesHome, !isLinux /* ignore case */)) { + return localize('untitledWorkspace', "Untitled Workspace"); + } + + return basename(workspace.configPath); +} \ No newline at end of file diff --git a/src/vs/platform/workspaces/common/workspacesIpc.ts b/src/vs/platform/workspaces/common/workspacesIpc.ts new file mode 100644 index 00000000000..225f1dc13e1 --- /dev/null +++ b/src/vs/platform/workspaces/common/workspacesIpc.ts @@ -0,0 +1,45 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { TPromise } from 'vs/base/common/winjs.base'; +import { IChannel } from 'vs/base/parts/ipc/common/ipc'; +import { IWorkspacesService, IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; + +export interface IWorkspacesChannel extends IChannel { + call(command: 'createWorkspace', arg: [string[]]): TPromise; + call(command: 'saveWorkspace', arg: [IWorkspaceIdentifier, string]): TPromise; + call(command: string, arg?: any): TPromise; +} + +export class WorkspacesChannel implements IWorkspacesChannel { + + constructor(private service: IWorkspacesService) { } + + call(command: string, arg?: any): TPromise { + switch (command) { + case 'createWorkspace': return this.service.createWorkspace(arg); + case 'saveWorkspace': return this.service.saveWorkspace(arg[0], arg[1]); + } + + return void 0; + } +} + +export class WorkspacesChannelClient implements IWorkspacesService { + + _serviceBrand: any; + + constructor(private channel: IWorkspacesChannel) { } + + createWorkspace(folders?: string[]): TPromise { + return this.channel.call('createWorkspace', folders); + } + + saveWorkspace(workspace: IWorkspaceIdentifier, target: string): TPromise { + return this.channel.call('saveWorkspace', [workspace, target]); + } +} \ No newline at end of file diff --git a/src/vs/platform/workspaces/electron-main/workspacesMainService.ts b/src/vs/platform/workspaces/electron-main/workspacesMainService.ts new file mode 100644 index 00000000000..ccb0b47705a --- /dev/null +++ b/src/vs/platform/workspaces/electron-main/workspacesMainService.ts @@ -0,0 +1,96 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { IWorkspacesMainService, IWorkspaceIdentifier, IStoredWorkspace, WORKSPACE_EXTENSION } from "vs/platform/workspaces/common/workspaces"; +import { TPromise } from "vs/base/common/winjs.base"; +import { isParent } from "vs/platform/files/common/files"; +import { IEnvironmentService } from "vs/platform/environment/common/environment"; +import { extname, join } from "path"; +import { mkdirp, writeFile, exists } from "vs/base/node/pfs"; +import { readFileSync } from "fs"; +import { isLinux } from "vs/base/common/platform"; +import { copy } from "vs/base/node/extfs"; +import { nfcall } from "vs/base/common/async"; +import { localize } from "vs/nls"; + +export class WorkspacesMainService implements IWorkspacesMainService { + + public _serviceBrand: any; + + protected workspacesHome: string; + + constructor( @IEnvironmentService private environmentService: IEnvironmentService) { + this.workspacesHome = environmentService.workspacesHome; + } + + public resolveWorkspaceSync(path: string): IWorkspaceIdentifier { + const isWorkspace = this.isInsideWorkspacesHome(path) || extname(path) === `.${WORKSPACE_EXTENSION}`; + if (!isWorkspace) { + return null; // does not look like a valid workspace config file + } + + try { + const workspace = JSON.parse(readFileSync(path, 'utf8')) as IStoredWorkspace; + if (typeof workspace.id !== 'string' || !Array.isArray(workspace.folders) || workspace.folders.length === 0) { + return null; // looks like an invalid workspace file + } + + return { + id: workspace.id, + configPath: path + }; + } catch (error) { + return null; // unable to read or parse as workspace file + } + } + + private isInsideWorkspacesHome(path: string): boolean { + return isParent(path, this.environmentService.workspacesHome, !isLinux /* ignore case */); + } + + public createWorkspace(folders: string[]): TPromise { + if (!folders.length) { + return TPromise.wrapError(new Error('Creating a workspace requires at least one folder.')); + } + + const workspaceId = this.nextWorkspaceId(); + const workspaceConfigFolder = join(this.workspacesHome, workspaceId); + const workspaceConfigPath = join(workspaceConfigFolder, 'workspace.json'); + + return mkdirp(workspaceConfigFolder).then(() => { + const storedWorkspace: IStoredWorkspace = { + id: workspaceId, + folders + }; + + return writeFile(workspaceConfigPath, JSON.stringify(storedWorkspace, null, '\t')).then(() => ({ + id: workspaceId, + configPath: workspaceConfigPath + })); + }); + } + + private nextWorkspaceId(): string { + return (Date.now() + Math.round(Math.random() * 1000)).toString(); + } + + public isUntitledWorkspace(workspace: IWorkspaceIdentifier): boolean { + return this.isInsideWorkspacesHome(workspace.configPath); + } + + public saveWorkspace(workspace: IWorkspaceIdentifier, target: string): TPromise { + return exists(target).then(exists => { + if (exists) { + return TPromise.wrapError(new Error(localize('targetExists', "A workspace with the same name already exists at the provided location."))); + } + + return nfcall(copy, workspace.configPath, target).then(() => { + return this.resolveWorkspaceSync(target); + }); + }); + } +} \ No newline at end of file diff --git a/src/vs/platform/workspaces/test/electron-main/workspacesMainService.test.ts b/src/vs/platform/workspaces/test/electron-main/workspacesMainService.test.ts new file mode 100644 index 00000000000..6bc88981552 --- /dev/null +++ b/src/vs/platform/workspaces/test/electron-main/workspacesMainService.test.ts @@ -0,0 +1,109 @@ +/*--------------------------------------------------------------------------------------------- + * 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 assert from 'assert'; +import fs = require('fs'); +import os = require('os'); +import path = require('path'); +import extfs = require('vs/base/node/extfs'); +import pfs = require('vs/base/node/pfs'); +import { EnvironmentService } from 'vs/platform/environment/node/environmentService'; +import { parseArgs } from 'vs/platform/environment/node/argv'; +import { WorkspacesMainService } from "vs/platform/workspaces/electron-main/workspacesMainService"; +import { IStoredWorkspace, WORKSPACE_EXTENSION } from "vs/platform/workspaces/common/workspaces"; + +class TestWorkspacesMainService extends WorkspacesMainService { + constructor(workspacesHome: string) { + super(new EnvironmentService(parseArgs(process.argv), process.execPath)); + + this.workspacesHome = workspacesHome; + } +} + +suite('WorkspacesMainService', () => { + const parentDir = path.join(os.tmpdir(), 'vsctests', 'service'); + const workspacesHome = path.join(parentDir, 'Workspaces'); + + let service: TestWorkspacesMainService; + + setup(done => { + service = new TestWorkspacesMainService(workspacesHome); + + // Delete any existing backups completely and then re-create it. + extfs.del(workspacesHome, os.tmpdir(), () => { + pfs.mkdirp(workspacesHome).then(() => { + done(); + }); + }); + }); + + teardown(done => { + extfs.del(workspacesHome, os.tmpdir(), done); + }); + + test('createWorkspace (no folders)', done => { + return service.createWorkspace([]).then(null, error => { + assert.ok(error); + + done(); + }); + }); + + test('createWorkspace (folders)', done => { + return service.createWorkspace([process.cwd(), os.tmpdir()]).then(workspace => { + assert.ok(workspace); + assert.ok(fs.existsSync(workspace.configPath)); + + const ws = JSON.parse(fs.readFileSync(workspace.configPath).toString()) as IStoredWorkspace; + assert.equal(ws.id, workspace.id); + assert.equal(ws.folders.length, 2); + assert.equal(ws.folders[0], process.cwd()); + assert.equal(ws.folders[1], os.tmpdir()); + + done(); + }); + }); + + test('resolveWorkspace', done => { + return service.createWorkspace([process.cwd(), os.tmpdir()]).then(workspace => { + + // is not resolved because config path is no in workspaces home + assert.ok(!service.resolveWorkspaceSync(workspace.configPath)); + + // make it a valid workspace path + const newPath = path.join(path.dirname(workspace.configPath), `workspace.${WORKSPACE_EXTENSION}`); + fs.renameSync(workspace.configPath, newPath); + workspace.configPath = newPath; + + const resolved = service.resolveWorkspaceSync(workspace.configPath); + assert.deepEqual(resolved, { id: workspace.id, configPath: workspace.configPath }); + + done(); + }); + }); + + test('saveWorkspace', done => { + return service.createWorkspace([process.cwd(), os.tmpdir()]).then(workspace => { + const workspaceConfigPath = path.join(os.tmpdir(), `myworkspace.${WORKSPACE_EXTENSION}`); + + return service.saveWorkspace(workspace, workspaceConfigPath).then(savedWorkspace => { + assert.equal(savedWorkspace.id, workspace.id); + assert.equal(savedWorkspace.configPath, workspaceConfigPath); + + const ws = JSON.parse(fs.readFileSync(savedWorkspace.configPath).toString()) as IStoredWorkspace; + assert.equal(ws.id, workspace.id); + assert.equal(ws.folders.length, 2); + assert.equal(ws.folders[0], process.cwd()); + assert.equal(ws.folders[1], os.tmpdir()); + + extfs.delSync(workspaceConfigPath); + + done(); + }); + }); + }); +}); \ No newline at end of file diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index 2f717e4ddbf..ba9ad985085 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -201,9 +201,13 @@ declare module 'vscode' { * Get a word-range at the given position. By default words are defined by * common separators, like space, -, _, etc. In addition, per languge custom * [word definitions](#LanguageConfiguration.wordPattern) can be defined. It - * is also possible to provide a custom regular expression. *Note* that a - * custom regular expression must not match the empty string and that it will - * be ignored if it does. + * is also possible to provide a custom regular expression. + * + * * *Note 1:* A custom regular expression must not match the empty string and + * if it does, it will be ignored. + * * *Note 2:* A custom regular expression will fail to match multiline strings + * and in the name of speed regular expressions should not match words with + * spaces. Use [`TextLine.text`](#TextLine.text) for more complex, non-wordy, scenarios. * * The position will be [adjusted](#TextDocument.validatePosition). * @@ -5352,6 +5356,11 @@ declare module 'vscode' { */ export interface DebugSession { + /** + * The unique ID of this debug session. + */ + readonly id: string; + /** * The debug session's type from the debug configuration. */ @@ -5368,21 +5377,60 @@ declare module 'vscode' { customRequest(command: string, args?: any): Thenable; } + /** + * A custom Debug Adapter Protocol event received from a [debug session](#DebugSession). + */ + export interface DebugSessionCustomEvent { + /** + * The [debug session](#DebugSession) for which the custom event was received. + */ + session: DebugSession; + + /** + * Type of event. + */ + event: string; + + /** + * Event specific information. + */ + body?: any; + } + /** * Namespace for dealing with debug sessions. */ export namespace debug { - /** - * An [event](#Event) which fires when a debug session has terminated. - */ - export const onDidTerminateDebugSession: Event; - /** * Create a new debug session based on the given configuration. * @param configuration */ export function createDebugSession(configuration: DebugConfiguration): Thenable; + + /** + * The currently active debug session or `undefined`. The active debug session is the one + * represented by the debug action floating window or the one currently shown in the drop down menu of the debug action floating window. + * If no debug session is active, the value is `undefined`. + */ + export let activeDebugSession: DebugSession | undefined; + + /** + * An [event](#Event) which fires when the [active debug session](#debug.activeDebugSession) + * has changed. *Note* that the event also fires when the active debug session changes + * to `undefined`. + */ + export const onDidChangeActiveDebugSession: Event; + + /** + * An [event](#Event) which fires when a custom DAP event is received from the debug session. + */ + export const onDidReceiveDebugSessionCustomEvent: Event; + + /** + * An [event](#Event) which fires when a debug session has terminated. + */ + export const onDidTerminateDebugSession: Event; } /** diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 6ae007cf28d..5b2ae8ba9de 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -8,8 +8,14 @@ declare module 'vscode' { export interface WorkspaceFoldersChangeEvent { - readonly addedFolders: Uri[]; - readonly removedFolders: Uri[]; + readonly added: WorkspaceFolder[]; + readonly removed: WorkspaceFolder[]; + } + + export interface WorkspaceFolder { + readonly uri: Uri; + readonly name: string; + readonly index: number; } export namespace workspace { @@ -18,12 +24,18 @@ declare module 'vscode' { * List of workspace folders or `undefined` when no folder is open. The *first* * element in the array is equal to the [`rootPath`](#workspace.rootPath) */ - export let workspaceFolders: Uri[] | undefined; + export let workspaceFolders: WorkspaceFolder[] | undefined; /** * An event that is emitted when a workspace folder is added or removed. */ export const onDidChangeWorkspaceFolders: Event; + + /** + * + * @param pathOrUri + */ + export function getContainingWorkspaceFolder(uri: Uri): WorkspaceFolder | undefined; } export interface WorkspaceConfiguration2 extends WorkspaceConfiguration { @@ -201,55 +213,4 @@ declare module 'vscode' { */ onData(callback: (data: string) => any): void; } - - /** - * A custom Debug Adapter Protocol event received from a [debug session](#DebugSession). - */ - export interface DebugSessionCustomEvent { - /** - * The [debug session](#DebugSession) for which the custom event was received. - */ - session: DebugSession; - - /** - * Type of event. - */ - event: string; - - /** - * Event specific information. - */ - body?: any; - } - - export namespace debug { - - /** - * The currently active debug session or `undefined`. The active debug session is the one - * represented by the debug action floating window or the one currently shown in the drop down menu of the debug action floating window. - * If no debug session is active, the value is `undefined`. - */ - export let activeDebugSession: DebugSession | undefined; - - /** - * An [event](#Event) which fires when the [active debug session](#debug.activeDebugSession) - * has changed. *Note* that the event also fires when the active debug session changes - * to `undefined`. - */ - export const onDidChangeActiveDebugSession: Event; - - /** - * An [event](#Event) which fires when a custom DAP event is received from the debug session. - */ - export const onDidReceiveDebugSessionCustomEvent: Event; - } - - export interface DebugSession { - - /** - * The debug session's ID. - */ - readonly id: string; - } - } diff --git a/src/vs/workbench/api/electron-browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/electron-browser/mainThreadLanguageFeatures.ts index 03934ca9335..57e178733b3 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadLanguageFeatures.ts @@ -157,7 +157,7 @@ export class MainThreadLanguageFeatures extends MainThreadLanguageFeaturesShape $registerQuickFixSupport(handle: number, selector: vscode.DocumentSelector): TPromise { this._registrations[handle] = modes.CodeActionProviderRegistry.register(selector, { - provideCodeActions: (model: IReadOnlyModel, range: EditorRange, token: CancellationToken): Thenable => { + provideCodeActions: (model: IReadOnlyModel, range: EditorRange, token: CancellationToken): Thenable => { return this._heapService.trackRecursive(wireCancellationToken(token, this._proxy.$provideCodeActions(handle, model.uri, range))); } }); diff --git a/src/vs/workbench/api/node/extHost.api.impl.ts b/src/vs/workbench/api/node/extHost.api.impl.ts index 332d8e24a24..b86c7ec1b98 100644 --- a/src/vs/workbench/api/node/extHost.api.impl.ts +++ b/src/vs/workbench/api/node/extHost.api.impl.ts @@ -131,6 +131,20 @@ export function createApiFactory( } } + const apiUsage = new class { + private _seen = new Set(); + publicLog(apiName: string) { + if (this._seen.has(apiName)) { + return undefined; + } + this._seen.add(apiName); + return telemetryService.publicLog('apiUsage', { + name: apiName, + extension: extension.id + }); + } + }; + // namespace: commands const commands: typeof vscode.commands = { registerCommand(id: string, command: (...args: any[]) => T | Thenable, thisArgs?: any): vscode.Disposable { @@ -347,28 +361,24 @@ export function createApiFactory( // namespace: workspace const workspace: typeof vscode.workspace = { get rootPath() { - telemetryService.publicLog('api-getter', { - name: 'workspace#rootPath', - extension: extension.id - }); + apiUsage.publicLog('workspace#rootPath'); return extHostWorkspace.getPath(); }, set rootPath(value) { throw errors.readonly(); }, + getContainingWorkspaceFolder(resource) { + return extHostWorkspace.getEnclosingWorkspaceFolder(resource); + }, get workspaceFolders() { + // proposed api assertProposedApi(extension); - telemetryService.publicLog('api-getter', { - name: 'workspace#workspaceFolders', - extension: extension.id - }); - return extHostWorkspace.getRoots(); + apiUsage.publicLog('workspace#workspaceFolders'); + return extHostWorkspace.getWorkspaceFolders(); }, onDidChangeWorkspaceFolders: proposedApiFunction(extension, (listener, thisArgs?, disposables?) => { - telemetryService.publicLog('api-getter', { - name: 'workspace#onDidChangeWorkspaceFolders', - extension: extension.id - }); + // proposed api + apiUsage.publicLog('workspace#onDidChangeWorkspaceFolders'); return extHostWorkspace.onDidChangeWorkspace(listener, thisArgs, disposables); }), asRelativePath: (pathOrUri) => { @@ -467,7 +477,6 @@ export function createApiFactory( // namespace: debug const debug: typeof vscode.debug = { get activeDebugSession() { - assertProposedApi(extension); return extHostDebugService.activeDebugSession; }, createDebugSession(config: vscode.DebugConfiguration) { @@ -476,12 +485,12 @@ export function createApiFactory( onDidTerminateDebugSession(listener, thisArg?, disposables?) { return extHostDebugService.onDidTerminateDebugSession(listener, thisArg, disposables); }, - onDidChangeActiveDebugSession: proposedApiFunction(extension, (listener, thisArg?, disposables?) => { + onDidChangeActiveDebugSession(listener, thisArg?, disposables?) { return extHostDebugService.onDidChangeActiveDebugSession(listener, thisArg, disposables); - }), - onDidReceiveDebugSessionCustomEvent: proposedApiFunction(extension, (listener, thisArg?, disposables?) => { + }, + onDidReceiveDebugSessionCustomEvent(listener, thisArg?, disposables?) { return extHostDebugService.onDidReceiveDebugSessionCustomEvent(listener, thisArg, disposables); - }) + } }; diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index 2ed8b299f98..5efd34a2fa7 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -468,7 +468,7 @@ export abstract class ExtHostLanguageFeaturesShape { $provideHover(handle: number, resource: URI, position: IPosition): TPromise { throw ni(); } $provideDocumentHighlights(handle: number, resource: URI, position: IPosition): TPromise { throw ni(); } $provideReferences(handle: number, resource: URI, position: IPosition, context: modes.ReferenceContext): TPromise { throw ni(); } - $provideCodeActions(handle: number, resource: URI, range: IRange): TPromise { throw ni(); } + $provideCodeActions(handle: number, resource: URI, range: IRange): TPromise { throw ni(); } $provideDocumentFormattingEdits(handle: number, resource: URI, options: modes.FormattingOptions): TPromise { throw ni(); } $provideDocumentRangeFormattingEdits(handle: number, resource: URI, range: IRange, options: modes.FormattingOptions): TPromise { throw ni(); } $provideOnTypeFormattingEdits(handle: number, resource: URI, position: IPosition, ch: string, options: modes.FormattingOptions): TPromise { throw ni(); } diff --git a/src/vs/workbench/api/node/extHostApiCommands.ts b/src/vs/workbench/api/node/extHostApiCommands.ts index 9f3e5af1cbc..c34a32838b7 100644 --- a/src/vs/workbench/api/node/extHostApiCommands.ts +++ b/src/vs/workbench/api/node/extHostApiCommands.ts @@ -399,11 +399,11 @@ export class ExtHostApiCommands { resource, range: typeConverters.fromRange(range) }; - return this._commands.executeCommand('_executeCodeActionProvider', args).then(value => { + return this._commands.executeCommand('_executeCodeActionProvider', args).then(value => { if (!Array.isArray(value)) { return undefined; } - return value.map(quickFix => this._commands.converter.fromInternal(quickFix.command)); + return value.map(quickFix => this._commands.converter.fromInternal(quickFix)); }); } diff --git a/src/vs/workbench/api/node/extHostLanguageFeatures.ts b/src/vs/workbench/api/node/extHostLanguageFeatures.ts index 2bf60e23f7a..620da77e832 100644 --- a/src/vs/workbench/api/node/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/node/extHostLanguageFeatures.ts @@ -271,7 +271,7 @@ class QuickFixAdapter { this._provider = provider; } - provideCodeActions(resource: URI, range: IRange): TPromise { + provideCodeActions(resource: URI, range: IRange): TPromise { const doc = this._documents.getDocumentData(resource).document; const ran = TypeConverters.toRange(range); @@ -291,12 +291,7 @@ class QuickFixAdapter { if (!Array.isArray(commands)) { return undefined; } - return commands.map((command, i) => { - return { - command: this._commands.toInternal(command), - score: i - }; - }); + return commands.map(command => this._commands.toInternal(command)); }); } } @@ -713,7 +708,7 @@ export class ExtHostLanguageFeatures extends ExtHostLanguageFeaturesShape { return ExtHostLanguageFeatures._handlePool++; } - private _withAdapter(handle: number, ctor: { new (...args: any[]): A }, callback: (adapter: A) => TPromise): TPromise { + private _withAdapter(handle: number, ctor: { new(...args: any[]): A }, callback: (adapter: A) => TPromise): TPromise { let adapter = this._adapter.get(handle); if (!(adapter instanceof ctor)) { return TPromise.wrapError(new Error('no adapter found')); @@ -843,7 +838,7 @@ export class ExtHostLanguageFeatures extends ExtHostLanguageFeaturesShape { return this._createDisposable(handle); } - $provideCodeActions(handle: number, resource: URI, range: IRange): TPromise { + $provideCodeActions(handle: number, resource: URI, range: IRange): TPromise { return this._withAdapter(handle, QuickFixAdapter, adapter => adapter.provideCodeActions(resource, range)); } diff --git a/src/vs/workbench/api/node/extHostWorkspace.ts b/src/vs/workbench/api/node/extHostWorkspace.ts index 2d9fbd7ab70..e02ea3b2bea 100644 --- a/src/vs/workbench/api/node/extHostWorkspace.ts +++ b/src/vs/workbench/api/node/extHostWorkspace.ts @@ -8,7 +8,7 @@ import URI from 'vs/base/common/uri'; import Event, { Emitter } from 'vs/base/common/event'; import { normalize } from 'vs/base/common/paths'; import { isFalsyOrEmpty, delta } from 'vs/base/common/arrays'; -import { relative } from 'path'; +import { relative, basename } from 'path'; import { Workspace } from 'vs/platform/workspace/common/workspace'; import { IThreadService } from 'vs/workbench/services/thread/common/threadService'; import { IResourceEdit } from 'vs/editor/common/services/bulkEdit'; @@ -20,36 +20,71 @@ import { compare } from "vs/base/common/strings"; import { asWinJsPromise } from 'vs/base/common/async'; import { Disposable } from 'vs/workbench/api/node/extHostTypes'; + +class Workspace2 { + + static fromData(data: IWorkspaceData) { + return data ? new Workspace2(data) : null; + } + + readonly workspace: Workspace; + readonly folders: vscode.WorkspaceFolder[]; + + private constructor(data: IWorkspaceData) { + this.workspace = new Workspace(data.id, data.name, data.roots); + this.folders = this.workspace.roots.map((uri, index) => ({ name: basename(uri.fsPath), uri, index })); + } + + getRoot(uri: URI): vscode.WorkspaceFolder { + const root = this.workspace.getRoot(uri); + if (root) { + for (const folder of this.folders) { + if (folder.uri.toString() === uri.toString()) { + return folder; + } + } + } + return undefined; + } +} + export class ExtHostWorkspace extends ExtHostWorkspaceShape { private static _requestIdPool = 0; private readonly _onDidChangeWorkspace = new Emitter(); private readonly _proxy: MainThreadWorkspaceShape; - private _workspace: Workspace; + private _workspace: Workspace2; readonly onDidChangeWorkspace: Event = this._onDidChangeWorkspace.event; constructor(threadService: IThreadService, data: IWorkspaceData) { super(); this._proxy = threadService.get(MainContext.MainThreadWorkspace); - this._workspace = data ? new Workspace(data.id, data.name, data.roots) : null; + this._workspace = Workspace2.fromData(data); } // --- workspace --- get workspace(): Workspace { - return this._workspace; + return this._workspace && this._workspace.workspace; } - getRoots(): URI[] { + getWorkspaceFolders(): vscode.WorkspaceFolder[] { if (!this._workspace) { return undefined; } else { - return this._workspace.roots.slice(0); + return this._workspace.folders.slice(0); } } + getEnclosingWorkspaceFolder(uri: vscode.Uri): vscode.WorkspaceFolder { + if (!this._workspace) { + return undefined; + } + return this._workspace.getRoot(uri); + } + getPath(): string { // this is legacy from the days before having // multi-root and we keep it only alive if there @@ -57,7 +92,7 @@ export class ExtHostWorkspace extends ExtHostWorkspaceShape { if (!this._workspace) { return undefined; } - const { roots } = this._workspace; + const { roots } = this._workspace.workspace; if (roots.length === 0) { return undefined; } @@ -82,11 +117,11 @@ export class ExtHostWorkspace extends ExtHostWorkspaceShape { return path; } - if (!this._workspace || isFalsyOrEmpty(this._workspace.roots)) { + if (!this._workspace || isFalsyOrEmpty(this._workspace.workspace.roots)) { return normalize(path); } - for (const { fsPath } of this._workspace.roots) { + for (const { fsPath } of this._workspace.workspace.roots) { let result = relative(fsPath, path); if (!result || result.indexOf('..') === 0) { continue; @@ -99,23 +134,23 @@ export class ExtHostWorkspace extends ExtHostWorkspaceShape { $acceptWorkspaceData(data: IWorkspaceData): void { - // compute delta - const oldRoots = this._workspace ? this._workspace.roots.sort(ExtHostWorkspace._compareUri) : []; - const newRoots = data ? data.roots.sort(ExtHostWorkspace._compareUri) : []; - const { added, removed } = delta(oldRoots, newRoots, ExtHostWorkspace._compareUri); + // keep old workspace folder, build new workspace, and + // capture new workspace folders. Compute delta between + // them send that as event + const oldRoots = this._workspace ? this._workspace.folders.sort(ExtHostWorkspace._compareWorkspaceFolder) : []; - // update state - this._workspace = data ? new Workspace(data.id, data.name, data.roots) : null; + this._workspace = Workspace2.fromData(data); + const newRoots = this._workspace ? this._workspace.folders.sort(ExtHostWorkspace._compareWorkspaceFolder) : []; - // send event + const { added, removed } = delta(oldRoots, newRoots, ExtHostWorkspace._compareWorkspaceFolder); this._onDidChangeWorkspace.fire(Object.freeze({ - addedFolders: Object.freeze(added), - removedFolders: Object.freeze(removed) + added: Object.freeze(added), + removed: Object.freeze(removed) })); } - private static _compareUri(a: vscode.Uri, b: vscode.Uri): number { - return compare(a.toString(), b.toString()); + private static _compareWorkspaceFolder(a: vscode.WorkspaceFolder, b: vscode.WorkspaceFolder): number { + return compare(a.uri.toString(), b.uri.toString()); } // --- search --- diff --git a/src/vs/workbench/browser/actions/fileActions.ts b/src/vs/workbench/browser/actions/fileActions.ts deleted file mode 100644 index b48f49d4a17..00000000000 --- a/src/vs/workbench/browser/actions/fileActions.ts +++ /dev/null @@ -1,104 +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 { TPromise } from 'vs/base/common/winjs.base'; -import { Action } from 'vs/base/common/actions'; -import nls = require('vs/nls'); -import { IWindowService } from 'vs/platform/windows/common/windows'; -import { ITelemetryData } from 'vs/platform/telemetry/common/telemetry'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { IWorkspaceEditingService } from 'vs/workbench/services/workspace/common/workspaceEditing'; -import URI from 'vs/base/common/uri'; -import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; - -export class OpenFolderAction extends Action { - - static ID = 'workbench.action.files.openFolder'; - static LABEL = nls.localize('openFolder', "Open Folder..."); - - constructor( - id: string, - label: string, - @IWindowService private windowService: IWindowService - ) { - super(id, label); - } - - run(event?: any, data?: ITelemetryData): TPromise { - return this.windowService.pickFolderAndOpen(undefined, data); - } -} - -export class OpenFileFolderAction extends Action { - - static ID = 'workbench.action.files.openFileFolder'; - static LABEL = nls.localize('openFileFolder', "Open..."); - - constructor( - id: string, - label: string, - @IWindowService private windowService: IWindowService - ) { - super(id, label); - } - - run(event?: any, data?: ITelemetryData): TPromise { - return this.windowService.pickFileFolderAndOpen(undefined, data); - } -} - -export class AddRootFolderAction extends Action { - - static ID = 'workbench.action.addRootFolder'; - static LABEL = nls.localize('addFolderToWorkspace', "Add Folder to Workspace..."); - - constructor( - id: string, - label: string, - @IWindowService private windowService: IWindowService, - @IWorkspaceContextService private contextService: IWorkspaceContextService, - @IWorkspaceEditingService private workspaceEditingService: IWorkspaceEditingService, - @IViewletService private viewletService: IViewletService - ) { - super(id, label); - } - - public run(): TPromise { - if (!this.contextService.hasWorkspace()) { - return this.windowService.pickFolderAndOpen(false /* prefer same window */); - } - - return this.windowService.pickFolder({ buttonLabel: nls.localize('add', "Add"), title: nls.localize('addFolderToWorkspaceTitle', "Add Folder to Workspace") }).then(folders => { - if (!folders.length) { - return TPromise.as(null); - } - - return this.workspaceEditingService.addRoots(folders.map(folder => URI.file(folder))).then(() => { - return this.viewletService.openViewlet(this.viewletService.getDefaultViewletId(), true); - }); - }); - } -} - -export class RemoveRootFolderAction extends Action { - - static ID = 'workbench.action.removeRootFolder'; - static LABEL = nls.localize('removeFolderFromWorkspace', "Remove Folder from Workspace"); - - constructor( - private rootUri: URI, - id: string, - label: string, - @IWorkspaceEditingService private workspaceEditingService: IWorkspaceEditingService - ) { - super(id, label); - } - - public run(): TPromise { - return this.workspaceEditingService.removeRoots([this.rootUri]); - } -} diff --git a/src/vs/workbench/browser/actions/workspaceActions.ts b/src/vs/workbench/browser/actions/workspaceActions.ts new file mode 100644 index 00000000000..8b0fdd0e6e7 --- /dev/null +++ b/src/vs/workbench/browser/actions/workspaceActions.ts @@ -0,0 +1,261 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { TPromise } from 'vs/base/common/winjs.base'; +import { Action } from 'vs/base/common/actions'; +import nls = require('vs/nls'); +import { IWindowService, IWindowsService } from 'vs/platform/windows/common/windows'; +import { ITelemetryData } from 'vs/platform/telemetry/common/telemetry'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { IWorkspaceEditingService } from 'vs/workbench/services/workspace/common/workspaceEditing'; +import URI from 'vs/base/common/uri'; +import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; +import { IInstantiationService } from "vs/platform/instantiation/common/instantiation"; +import { WORKSPACE_EXTENSION, IWorkspacesService } from "vs/platform/workspaces/common/workspaces"; +import { IEnvironmentService } from "vs/platform/environment/common/environment"; +import { isWindows, isLinux } from "vs/base/common/platform"; + +export class OpenFolderAction extends Action { + + static ID = 'workbench.action.files.openFolder'; + static LABEL = nls.localize('openFolder', "Open Folder..."); + + constructor( + id: string, + label: string, + @IWindowService private windowService: IWindowService + ) { + super(id, label); + } + + run(event?: any, data?: ITelemetryData): TPromise { + return this.windowService.pickFolderAndOpen(undefined, data); + } +} + +export class OpenFileFolderAction extends Action { + + static ID = 'workbench.action.files.openFileFolder'; + static LABEL = nls.localize('openFileFolder', "Open..."); + + constructor( + id: string, + label: string, + @IWindowService private windowService: IWindowService + ) { + super(id, label); + } + + run(event?: any, data?: ITelemetryData): TPromise { + return this.windowService.pickFileFolderAndOpen(undefined, data); + } +} + +export abstract class BaseRootFolderAction extends Action { + + constructor( + id: string, + label: string, + protected windowService: IWindowService, + protected instantiationService: IInstantiationService, + protected environmentService: IEnvironmentService + ) { + super(id, label); + } + + protected handleNotInMultiFolderWorkspaceCase(message: string): TPromise { + const newWorkspace = { label: this.mnemonicLabel(nls.localize({ key: 'create', comment: ['&& denotes a mnemonic'] }, "&&New Workspace")), canceled: false }; + const cancel = { label: nls.localize('cancel', "Cancel"), canceled: true }; + + const buttons: { label: string; canceled: boolean; }[] = []; + if (isLinux) { + buttons.push(cancel, newWorkspace); + } else { + buttons.push(newWorkspace, cancel); + } + + const opts: Electron.ShowMessageBoxOptions = { + title: this.environmentService.appNameLong, + message, + detail: nls.localize('workspaceDetail', "Workspaces allow to open multiple folders at once."), + noLink: true, + type: 'info', + buttons: buttons.map(button => button.label), + cancelId: buttons.indexOf(cancel) + }; + + if (isLinux) { + opts.defaultId = 1; + } + + const res = this.windowService.showMessageBox(opts); + if (!buttons[res].canceled) { + return this.instantiationService.createInstance(NewWorkspaceAction, NewWorkspaceAction.ID, NewWorkspaceAction.LABEL).run(); + } + + return TPromise.as(null); + } + + private mnemonicLabel(label: string): string { + if (!isWindows) { + return label.replace(/\(&&\w\)|&&/g, ''); // no mnemonic support on mac/linux + } + + return label.replace(/&&/g, '&'); + } +} + +export class AddRootFolderAction extends BaseRootFolderAction { + + static ID = 'workbench.action.addRootFolder'; + static LABEL = nls.localize('addFolderToWorkspace', "Add Folder to Workspace..."); + + constructor( + id: string, + label: string, + @IWindowService windowService: IWindowService, + @IInstantiationService instantiationService: IInstantiationService, + @IWorkspaceContextService private contextService: IWorkspaceContextService, + @IWorkspaceEditingService private workspaceEditingService: IWorkspaceEditingService, + @IViewletService private viewletService: IViewletService, + @IEnvironmentService environmentService: IEnvironmentService + ) { + super(id, label, windowService, instantiationService, environmentService); + } + + public run(): TPromise { + if (!this.contextService.hasMultiFolderWorkspace()) { + return super.handleNotInMultiFolderWorkspaceCase(nls.localize('addSupported', "You can only add folders to workspaces. Do you want to create a new workspace?")); + } + + return this.windowService.pickFolder({ buttonLabel: nls.localize('add', "Add"), title: nls.localize('addFolderToWorkspaceTitle', "Add Folder to Workspace") }).then(folders => { + if (!folders.length) { + return TPromise.as(null); + } + + return this.workspaceEditingService.addRoots(folders.map(folder => URI.file(folder))).then(() => { + return this.viewletService.openViewlet(this.viewletService.getDefaultViewletId(), true); + }); + }); + } +} + +export class NewWorkspaceAction extends Action { + + static ID = 'workbench.action.newWorkspace'; + static LABEL = nls.localize('newWorkspace', "New Workspace..."); + + constructor( + id: string, + label: string, + @IWindowService private windowService: IWindowService, + @IWorkspaceEditingService private workspaceEditingService: IWorkspaceEditingService, + ) { + super(id, label); + } + + public run(): TPromise { + return this.windowService.pickFolder({ buttonLabel: nls.localize('select', "Select"), title: nls.localize('selectWorkspace', "Select Folders for Workspace") }).then(folders => { + if (!folders.length) { + return TPromise.as(null); + } + + return this.workspaceEditingService.createAndOpenWorkspace(folders.map(folder => URI.file(folder))); + }); + } +} + +export class RemoveRootFolderAction extends Action { + + static ID = 'workbench.action.removeRootFolder'; + static LABEL = nls.localize('removeFolderFromWorkspace', "Remove Folder from Workspace"); + + constructor( + private rootUri: URI, + id: string, + label: string, + @IWorkspaceEditingService private workspaceEditingService: IWorkspaceEditingService + ) { + super(id, label); + } + + public run(): TPromise { + return this.workspaceEditingService.removeRoots([this.rootUri]); + } +} + +const codeWorkspaceFilter = [{ name: nls.localize('codeWorkspace', "Code Workspace"), extensions: [WORKSPACE_EXTENSION] }]; + +export class SaveWorkspaceAction extends BaseRootFolderAction { + + static ID = 'workbench.action.saveWorkspace'; + static LABEL = nls.localize('saveWorkspaceAction', "Save Workspace..."); + + constructor( + id: string, + label: string, + @IWindowService windowService: IWindowService, + @IEnvironmentService environmentService: IEnvironmentService, + @IWorkspaceContextService private contextService: IWorkspaceContextService, + @IWorkspacesService private workspacesService: IWorkspacesService, + @IInstantiationService instantiationService: IInstantiationService, + @IWindowsService private windowsService: IWindowsService + ) { + super(id, label, windowService, instantiationService, environmentService); + } + + public run(): TPromise { + if (!this.contextService.hasMultiFolderWorkspace()) { + return super.handleNotInMultiFolderWorkspaceCase(nls.localize('saveNotSupported', "You need to open a workspace first to save it. Do you want to create a new workspace?")); + } + + const target = this.windowService.showSaveDialog({ + buttonLabel: nls.localize('save', "Save"), + title: nls.localize('saveWorkspace', "Save Workspace"), + filters: codeWorkspaceFilter + }); + + if (target) { + const workspace = this.contextService.getWorkspace(); + return this.workspacesService.saveWorkspace({ id: workspace.id, configPath: workspace.configuration.fsPath }, target).then(workspace => { + return this.windowsService.openWindow([workspace.configPath]); + }); + } + + return TPromise.as(false); + } +} + +export class OpenWorkspaceAction extends Action { + + static ID = 'workbench.action.openWorkspace'; + static LABEL = nls.localize('openWorkspaceAction', "Open Workspace..."); + + constructor( + id: string, + label: string, + @IWindowService private windowService: IWindowService, + @IWindowsService private windowsService: IWindowsService + ) { + super(id, label); + } + + public run(): TPromise { + const files = this.windowService.showOpenDialog({ + buttonLabel: nls.localize('open', "Open"), + title: nls.localize('openWorkspace', "Open Workspace"), + filters: codeWorkspaceFilter, + properties: ['openFile'] + }); + + if (!files || !files.length) { + return TPromise.as(null); + } + + return this.windowsService.openWindow([files[0]]); + } +} \ No newline at end of file diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts index cc732f7305f..4265d0730ae 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts @@ -86,7 +86,6 @@ export class ActivitybarPart extends Part implements IActivityBarService { // TODO@Ben: Migrate git => scm viewlet .map(id => id === 'workbench.view.git' ? 'workbench.view.scm' : id) .filter(arrays.uniqueFilter(str => str)); - } else { this.pinnedViewlets = this.viewletService.getViewlets().map(v => v.id); } diff --git a/src/vs/workbench/browser/parts/compositePart.ts b/src/vs/workbench/browser/parts/compositePart.ts index 32a714eb3c9..dcd59fa022b 100644 --- a/src/vs/workbench/browser/parts/compositePart.ts +++ b/src/vs/workbench/browser/parts/compositePart.ts @@ -20,7 +20,7 @@ import errors = require('vs/base/common/errors'); import * as DOM from 'vs/base/browser/dom'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { CONTEXT as ToolBarContext, ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; -import { IActionItem, ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar'; +import { IActionItem, ActionsOrientation, Separator } from 'vs/base/browser/ui/actionbar/actionbar'; import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar'; import { IActionBarRegistry, Extensions, prepareActions } from 'vs/workbench/browser/actions'; import { Action, IAction } from 'vs/base/common/actions'; @@ -39,6 +39,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { attachProgressBarStyler } from 'vs/platform/theme/common/styler'; +import { ToggleSidebarVisibilityAction } from 'vs/workbench/browser/actions/toggleSidebarVisibility'; export interface ICompositeTitleLabel { @@ -478,6 +479,10 @@ export abstract class CompositePart extends Part { private onContextMenu(event: StandardMouseEvent): void { const contextMenuActions = this.activeComposite ? this.activeComposite.getContextMenuActions() : []; + if (contextMenuActions.length) { + contextMenuActions.push(new Separator()); + } + contextMenuActions.push(this.createHideSideBarAction()); if (contextMenuActions.length) { let anchor: { x: number, y: number } = { x: event.posx, y: event.posy }; this.contextMenuService.showContextMenu({ @@ -490,6 +495,15 @@ export abstract class CompositePart extends Part { } } + private createHideSideBarAction(): IAction { + return { + id: ToggleSidebarVisibilityAction.ID, + label: nls.localize('compositePart.hideSideBarLabel', "Hide Side Bar"), + enabled: true, + run: () => this.partService.setSideBarHidden(true) + }; + } + private actionItemProvider(action: Action): IActionItem { let actionItem: IActionItem; diff --git a/src/vs/workbench/browser/parts/editor/editor.contribution.ts b/src/vs/workbench/browser/parts/editor/editor.contribution.ts index 0e6fd0d5394..3cde1b68478 100644 --- a/src/vs/workbench/browser/parts/editor/editor.contribution.ts +++ b/src/vs/workbench/browser/parts/editor/editor.contribution.ts @@ -338,7 +338,7 @@ registry.registerWorkbenchAction(new SyncActionDescriptor(ShowEditorsInGroupThre registry.registerWorkbenchAction(new SyncActionDescriptor(OpenNextEditor, OpenNextEditor.ID, OpenNextEditor.LABEL, { primary: KeyMod.CtrlCmd | KeyCode.PageDown, mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.RightArrow, secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.US_CLOSE_SQUARE_BRACKET] } }), 'View: Open Next Editor', category); registry.registerWorkbenchAction(new SyncActionDescriptor(OpenPreviousEditor, OpenPreviousEditor.ID, OpenPreviousEditor.LABEL, { primary: KeyMod.CtrlCmd | KeyCode.PageUp, mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.LeftArrow, secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.US_OPEN_SQUARE_BRACKET] } }), 'View: Open Previous Editor', category); registry.registerWorkbenchAction(new SyncActionDescriptor(ReopenClosedEditorAction, ReopenClosedEditorAction.ID, ReopenClosedEditorAction.LABEL, { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_T }), 'View: Reopen Closed Editor', category); -registry.registerWorkbenchAction(new SyncActionDescriptor(ClearRecentFilesAction, ClearRecentFilesAction.ID, ClearRecentFilesAction.LABEL), 'View: Clear Recent Files', category); +registry.registerWorkbenchAction(new SyncActionDescriptor(ClearRecentFilesAction, ClearRecentFilesAction.ID, ClearRecentFilesAction.LABEL), 'View: Clear Recently Opened', category); registry.registerWorkbenchAction(new SyncActionDescriptor(KeepEditorAction, KeepEditorAction.ID, KeepEditorAction.LABEL, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.Enter) }), 'View: Keep Editor', category); registry.registerWorkbenchAction(new SyncActionDescriptor(CloseAllEditorsAction, CloseAllEditorsAction.ID, CloseAllEditorsAction.LABEL, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_W) }), 'View: Close All Editors', category); registry.registerWorkbenchAction(new SyncActionDescriptor(CloseLeftEditorsInGroupAction, CloseLeftEditorsInGroupAction.ID, CloseLeftEditorsInGroupAction.LABEL), 'View: Close Editors to the Left', category); diff --git a/src/vs/workbench/browser/parts/editor/editorActions.ts b/src/vs/workbench/browser/parts/editor/editorActions.ts index fe71352d509..694c02bdde0 100644 --- a/src/vs/workbench/browser/parts/editor/editorActions.ts +++ b/src/vs/workbench/browser/parts/editor/editorActions.ts @@ -1139,7 +1139,7 @@ export class ReopenClosedEditorAction extends Action { export class ClearRecentFilesAction extends Action { public static ID = 'workbench.action.clearRecentFiles'; - public static LABEL = nls.localize('clearRecentFiles', "Clear Recent Files"); + public static LABEL = nls.localize('clearRecentFiles', "Clear Recently Opened"); constructor( id: string, @@ -1150,7 +1150,7 @@ export class ClearRecentFilesAction extends Action { } public run(): TPromise { - this.windowsService.clearRecentPathsList(); + this.windowsService.clearRecentlyOpened(); return TPromise.as(false); } diff --git a/src/vs/workbench/browser/parts/editor/editorGroupsControl.ts b/src/vs/workbench/browser/parts/editor/editorGroupsControl.ts index 686faba3477..67ea8dad360 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupsControl.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupsControl.ts @@ -1118,12 +1118,7 @@ export class EditorGroupsControl extends Themable implements IEditorGroupsContro // Add external ones to recently open list const externalResources = droppedResources.filter(d => d.isExternal).map(d => d.resource); if (externalResources.length) { - $this.windowsService.addToRecentlyOpen(externalResources.map(resource => { - return { - path: resource.fsPath, - isFile: true - }; - })); + $this.windowsService.addRecentlyOpened(externalResources.map(resource => resource.fsPath)); } // Open in Editor diff --git a/src/vs/workbench/browser/parts/editor/editorPart.ts b/src/vs/workbench/browser/parts/editor/editorPart.ts index bbb9c345a6f..6c625a26b61 100644 --- a/src/vs/workbench/browser/parts/editor/editorPart.ts +++ b/src/vs/workbench/browser/parts/editor/editorPart.ts @@ -41,6 +41,9 @@ import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/c import { IThemeService } from 'vs/platform/theme/common/themeService'; import { editorBackground } from 'vs/platform/theme/common/colorRegistry'; import { EDITOR_GROUP_BACKGROUND } from 'vs/workbench/common/theme'; +import { createCSSRule } from "vs/base/browser/dom"; +import { IEnvironmentService } from "vs/platform/environment/common/environment"; +import { join } from "vs/base/common/paths"; class ProgressMonitor { @@ -122,7 +125,8 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupService @IConfigurationService private configurationService: IConfigurationService, @IContextKeyService contextKeyService: IContextKeyService, @IInstantiationService private instantiationService: IInstantiationService, - @IThemeService themeService: IThemeService + @IThemeService themeService: IThemeService, + @IEnvironmentService private environmentService: IEnvironmentService ) { super(id, { hasTitle: false }, themeService); @@ -172,9 +176,18 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupService this.revealIfOpen = false; } + this.initStyles(); this.registerListeners(); } + private initStyles(): void { + + // Letterpress Background when Empty + createCSSRule('.vs .monaco-workbench > .part.editor.empty', `background-image: url('${join(this.environmentService.appRoot, 'resources/letterpress.svg')}')`); + createCSSRule('.vs-dark .monaco-workbench > .part.editor.empty', `background-image: url('${join(this.environmentService.appRoot, 'resources/letterpress-dark.svg')}')`); + createCSSRule('.hc-black .monaco-workbench > .part.editor.empty', `background-image: url('${join(this.environmentService.appRoot, 'resources/letterpress-hc.svg')}')`); + } + private registerListeners(): void { this.toUnbind.push(this.stacks.onEditorDirty(identifier => this.onEditorDirty(identifier))); this.toUnbind.push(this.stacks.onEditorDisposed(identifier => this.onEditorDisposed(identifier))); diff --git a/src/vs/workbench/browser/parts/editor/media/editorpart.css b/src/vs/workbench/browser/parts/editor/media/editorpart.css index be300080fdd..2af9d3061b9 100644 --- a/src/vs/workbench/browser/parts/editor/media/editorpart.css +++ b/src/vs/workbench/browser/parts/editor/media/editorpart.css @@ -3,27 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.monaco-workbench > .part.editor { +/** Letter press styling for empty editor */ +.monaco-workbench > .part.editor.empty { background-repeat: no-repeat; background-position: 50% 50%; -} - -.monaco-workbench > .part.editor.empty { - background-image: url('letterpress.svg'); -} - -.vs-dark .monaco-workbench > .part.editor.empty { - background-image: url('letterpress-dark.svg'); -} - -.hc-black .monaco-workbench > .part.editor.empty { - background-image: url('letterpress-hc.svg'); -} - -@media -(-webkit-min-device-pixel-ratio: 2), -(min-resolution: 192dppx) { - .monaco-workbench > .part.editor { - background-size: 260px 260px; - } + background-size: 260px 260px; } \ No newline at end of file diff --git a/src/vs/workbench/browser/parts/editor/media/letterpress-dark.svg b/src/vs/workbench/browser/parts/editor/media/letterpress-dark.svg deleted file mode 100644 index 5cd4a656db5..00000000000 --- a/src/vs/workbench/browser/parts/editor/media/letterpress-dark.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/vs/workbench/browser/parts/editor/media/letterpress-hc.svg b/src/vs/workbench/browser/parts/editor/media/letterpress-hc.svg deleted file mode 100644 index 38b46ee704a..00000000000 --- a/src/vs/workbench/browser/parts/editor/media/letterpress-hc.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/vs/workbench/browser/parts/editor/media/letterpress.svg b/src/vs/workbench/browser/parts/editor/media/letterpress.svg deleted file mode 100644 index 41a76de75b0..00000000000 --- a/src/vs/workbench/browser/parts/editor/media/letterpress.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts index 41ab06226f9..242abde192d 100644 --- a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts @@ -691,12 +691,7 @@ export class TabsTitleControl extends TitleControl { // Add external ones to recently open list const externalResources = resources.filter(d => d.isExternal).map(d => d.resource); if (externalResources.length) { - this.windowsService.addToRecentlyOpen(externalResources.map(resource => { - return { - path: resource.fsPath, - isFile: true - }; - })); + this.windowsService.addRecentlyOpened(externalResources.map(resource => resource.fsPath)); } // Open in Editor diff --git a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts index 6e89598086f..fd2bb332339 100644 --- a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts +++ b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts @@ -27,7 +27,7 @@ import { getCodeEditor } from 'vs/editor/common/services/codeEditorService'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { Action } from 'vs/base/common/actions'; import { IThemeService, registerThemingParticipant, ITheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; -import { STATUS_BAR_BACKGROUND, STATUS_BAR_FOREGROUND, STATUS_BAR_NO_FOLDER_BACKGROUND, STATUS_BAR_ITEM_HOVER_BACKGROUND, STATUS_BAR_ITEM_ACTIVE_BACKGROUND, STATUS_BAR_PROMINENT_ITEM_BACKGROUND, STATUS_BAR_PROMINENT_ITEM_HOVER_BACKGROUND, STATUS_BAR_BORDER, STATUS_BAR_NO_FOLDER_FOREGROUND } from 'vs/workbench/common/theme'; +import { STATUS_BAR_BACKGROUND, STATUS_BAR_FOREGROUND, STATUS_BAR_NO_FOLDER_BACKGROUND, STATUS_BAR_MULTI_FOLDER_BACKGROUND, STATUS_BAR_MULTI_FOLDER_FOREGROUND, STATUS_BAR_ITEM_HOVER_BACKGROUND, STATUS_BAR_ITEM_ACTIVE_BACKGROUND, STATUS_BAR_PROMINENT_ITEM_BACKGROUND, STATUS_BAR_PROMINENT_ITEM_HOVER_BACKGROUND, STATUS_BAR_BORDER, STATUS_BAR_NO_FOLDER_FOREGROUND } from 'vs/workbench/common/theme'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { contrastBorder } from 'vs/platform/theme/common/colorRegistry'; import { isThemeColor } from 'vs/editor/common/editorCommon'; @@ -138,8 +138,8 @@ export class StatusbarPart extends Part implements IStatusbarService { const container = this.getContainer(); - container.style('color', this.getColor(this.contextService.hasWorkspace() ? STATUS_BAR_FOREGROUND : STATUS_BAR_NO_FOLDER_FOREGROUND)); - container.style('background-color', this.getColor(this.contextService.hasWorkspace() ? STATUS_BAR_BACKGROUND : STATUS_BAR_NO_FOLDER_BACKGROUND)); + container.style('color', this.getColor(this.contextService.hasMultiFolderWorkspace() ? STATUS_BAR_MULTI_FOLDER_FOREGROUND : this.contextService.hasWorkspace() ? STATUS_BAR_FOREGROUND : STATUS_BAR_NO_FOLDER_FOREGROUND)); + container.style('background-color', this.getColor(this.contextService.hasMultiFolderWorkspace() ? STATUS_BAR_MULTI_FOLDER_BACKGROUND : this.contextService.hasWorkspace() ? STATUS_BAR_BACKGROUND : STATUS_BAR_NO_FOLDER_BACKGROUND)); const borderColor = this.getColor(STATUS_BAR_BORDER) || this.getColor(contrastBorder); container.style('border-top-width', borderColor ? '1px' : null); diff --git a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts index b6639f3e00e..94a55583f65 100644 --- a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts +++ b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts @@ -187,12 +187,12 @@ export class TitlebarPart extends Part implements ITitleService { // Compute root resource // Single Root Workspace: always the single root workspace in this case - // Multi Root Workspace: not yet defined (TODO@Ben multi root) + // Multi Root Workspace: workspace configuration file let root: URI; - if (workspace) { - if (workspace.roots.length === 1) { - root = workspace.roots[0]; - } + if (this.contextService.hasMultiFolderWorkspace()) { + root = workspace.configuration; + } else if (this.contextService.hasFolderWorkspace()) { + root = workspace.roots[0]; } // Compute folder resource diff --git a/src/vs/workbench/common/theme.ts b/src/vs/workbench/common/theme.ts index 1aa4d17a4d5..f84e5d06a81 100644 --- a/src/vs/workbench/common/theme.ts +++ b/src/vs/workbench/common/theme.ts @@ -154,6 +154,18 @@ export const STATUS_BAR_NO_FOLDER_BACKGROUND = registerColor('statusBar.noFolder hc: null }, nls.localize('statusBarNoFolderBackground', "Status bar background color when no folder is opened. The status bar is shown in the bottom of the window.")); +export const STATUS_BAR_MULTI_FOLDER_BACKGROUND = registerColor('statusBar.multiFolderBackground', { + dark: '#2c4681', + light: '#2c4681', + hc: null +}, nls.localize('statusBarMultiFolderBackground', "Status bar background color when a workspace with multiple folders is opened. The status bar is shown in the bottom of the window.")); + +export const STATUS_BAR_MULTI_FOLDER_FOREGROUND = registerColor('statusBar.multiFolderForeground', { + dark: STATUS_BAR_FOREGROUND, + light: STATUS_BAR_FOREGROUND, + hc: STATUS_BAR_FOREGROUND +}, nls.localize('statusBarMultiFolderForeground', "Status bar foreground color when a workspace with multiple folders is opened. The status bar is shown in the bottom of the window.")); + export const STATUS_BAR_NO_FOLDER_FOREGROUND = registerColor('statusBar.noFolderForeground', { dark: STATUS_BAR_FOREGROUND, light: STATUS_BAR_FOREGROUND, diff --git a/src/vs/workbench/electron-browser/actions.ts b/src/vs/workbench/electron-browser/actions.ts index 073a76a5079..838e2790cf9 100644 --- a/src/vs/workbench/electron-browser/actions.ts +++ b/src/vs/workbench/electron-browser/actions.ts @@ -40,6 +40,7 @@ import { webFrame } from 'electron'; import { getPathLabel } from 'vs/base/common/labels'; import { IViewlet } from 'vs/workbench/common/viewlet'; import { IPanel } from 'vs/workbench/common/panel'; +import { IWorkspaceIdentifier, getWorkspaceLabel, ISingleFolderWorkspaceIdentifier } from "vs/platform/workspaces/common/workspaces"; // --- actions @@ -82,10 +83,10 @@ export class CloseWindowAction extends Action { } } -export class CloseFolderAction extends Action { +export class CloseWorkspaceAction extends Action { static ID = 'workbench.action.closeFolder'; - static LABEL = nls.localize('closeFolder', "Close Folder"); + static LABEL = nls.localize('closeWorkspace', "Close Workspace"); constructor( id: string, @@ -99,11 +100,12 @@ export class CloseFolderAction extends Action { run(): TPromise { if (!this.contextService.hasWorkspace()) { - this.messageService.show(Severity.Info, nls.localize('noFolderOpened', "There is currently no folder opened in this instance to close.")); + this.messageService.show(Severity.Info, nls.localize('noWorkspaceOpened', "There is currently no workspace opened in this instance to close.")); + return TPromise.as(null); } - return this.windowService.closeFolder(); + return this.windowService.closeWorkspace(); } } @@ -574,11 +576,11 @@ export abstract class BaseSwitchWindow extends Action { public run(): TPromise { const currentWindowId = this.windowService.getCurrentWindowId(); - return this.windowsService.getWindows().then(workspaces => { + return this.windowsService.getWindows().then(windows => { const placeHolder = nls.localize('switchWindowPlaceHolder', "Select a window to switch to"); - const picks = workspaces.map(win => ({ - resource: win.filename ? URI.file(win.filename) : win.path, - isFolder: !win.filename && !!win.path, + const picks = windows.map(win => ({ + resource: win.filename ? URI.file(win.filename) : win.folderPath ? URI.file(win.folderPath) : win.workspace ? URI.file(win.workspace.configPath) : void 0, + isFolder: !win.workspace && !win.filename && !!win.folderPath, label: win.title, description: (currentWindowId === win.id) ? nls.localize('current', "Current Window") : void 0, run: () => { @@ -662,18 +664,22 @@ export abstract class BaseOpenRecentAction extends Action { protected abstract isQuickNavigate(): boolean; public run(): TPromise { - return this.windowService.getRecentlyOpen() - .then(({ files, folders }) => this.openRecent(files, folders)); + return this.windowService.getRecentlyOpened() + .then(({ workspaces, files }) => this.openRecent(workspaces, files)); } - private openRecent(recentFiles: string[], recentFolders: string[]): void { + private openRecent(recentWorkspaces: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier)[], recentFiles: string[]): void { + + function toPick(arg1: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier, separator: ISeparator, isFolder: boolean, environmentService: IEnvironmentService): IFilePickOpenEntry { + const path = (typeof arg1 === 'string') ? arg1 : arg1.configPath; + const label = (typeof arg1 === 'string') ? paths.basename(path) : getWorkspaceLabel(environmentService, arg1); + const description = (typeof arg1 === 'string') ? getPathLabel(paths.dirname(path), null, environmentService) : void 0; - function toPick(path: string, separator: ISeparator, isFolder: boolean, environmentService: IEnvironmentService): IFilePickOpenEntry { return { resource: URI.file(path), isFolder, - label: paths.basename(path), - description: getPathLabel(paths.dirname(path), null, environmentService), + label, + description, separator, run: context => { setTimeout(() => { @@ -685,20 +691,20 @@ export abstract class BaseOpenRecentAction extends Action { }; } - const runPick = (path: string, context: IEntryRunContext) => { + const runPick = (arg1: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier, context: IEntryRunContext) => { const forceNewWindow = context.keymods.indexOf(KeyMod.CtrlCmd) >= 0; - this.windowsService.openWindow([path], { forceNewWindow }); + this.windowsService.openWindow([typeof arg1 === 'string' ? arg1 : arg1.configPath], { forceNewWindow }); }; - const folderPicks: IFilePickOpenEntry[] = recentFolders.map((p, index) => toPick(p, index === 0 ? { label: nls.localize('folders', "folders") } : void 0, true, this.environmentService)); + const workspacePicks: IFilePickOpenEntry[] = recentWorkspaces.map((workspace, index) => toPick(workspace, index === 0 ? { label: nls.localize('workspaces', "workspaces") } : void 0, true, this.environmentService)); const filePicks: IFilePickOpenEntry[] = recentFiles.map((p, index) => toPick(p, index === 0 ? { label: nls.localize('files', "files"), border: true } : void 0, false, this.environmentService)); const hasWorkspace = this.contextService.hasWorkspace(); - this.quickOpenService.pick(folderPicks.concat(...filePicks), { + this.quickOpenService.pick([...workspacePicks, ...filePicks], { contextKey: inRecentFilesPickerContextKey, autoFocus: { autoFocusFirstEntry: !hasWorkspace, autoFocusSecondEntry: hasWorkspace }, - placeHolder: isMacintosh ? nls.localize('openRecentPlaceHolderMac', "Select a path (hold Cmd-key to open in new window)") : nls.localize('openRecentPlaceHolder', "Select a path to open (hold Ctrl-key to open in new window)"), + placeHolder: isMacintosh ? nls.localize('openRecentPlaceHolderMac', "Select to open (hold Cmd-key to open in new window)") : nls.localize('openRecentPlaceHolder', "Select to open (hold Ctrl-key to open in new window)"), matchOnDescription: true, quickNavigateConfiguration: this.isQuickNavigate() ? { keybindings: this.keybindingService.lookupKeybindings(this.id) } : void 0 }).done(null, errors.onUnexpectedError); diff --git a/src/vs/workbench/electron-browser/bootstrap/index.js b/src/vs/workbench/electron-browser/bootstrap/index.js index 565b1e48c63..9ea85c5a8a8 100644 --- a/src/vs/workbench/electron-browser/bootstrap/index.js +++ b/src/vs/workbench/electron-browser/bootstrap/index.js @@ -214,8 +214,9 @@ function main() { // loads as soon as the loader loads. To be able to have pseudo translation const loaderTimer = startTimer('load:loader'); if (typeof Monaco_Loader_Init === 'function') { + const loader = Monaco_Loader_Init(); //eslint-disable-next-line no-global-assign - define = Monaco_Loader_Init(); + define = loader.define; require = loader.require; onLoader(); } else { diff --git a/src/vs/workbench/electron-browser/main.contribution.ts b/src/vs/workbench/electron-browser/main.contribution.ts index cdd17772514..39cc7368911 100644 --- a/src/vs/workbench/electron-browser/main.contribution.ts +++ b/src/vs/workbench/electron-browser/main.contribution.ts @@ -14,11 +14,11 @@ import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'v import { IWorkbenchActionRegistry, Extensions } from 'vs/workbench/common/actionRegistry'; import { KeyMod, KeyChord, KeyCode } from 'vs/base/common/keyCodes'; import { isWindows, isLinux, isMacintosh } from 'vs/base/common/platform'; -import { CloseEditorAction, KeybindingsReferenceAction, OpenDocumentationUrlAction, OpenIntroductoryVideosUrlAction, OpenTipsAndTricksUrlAction, ReportIssueAction, ReportPerformanceIssueAction, ZoomResetAction, ZoomOutAction, ZoomInAction, ToggleFullScreenAction, ToggleMenuBarAction, CloseFolderAction, CloseWindowAction, SwitchWindow, NewWindowAction, CloseMessagesAction, NavigateUpAction, NavigateDownAction, NavigateLeftAction, NavigateRightAction, IncreaseViewSizeAction, DecreaseViewSizeAction, ShowStartupPerformance, ToggleSharedProcessAction, QuickSwitchWindow, QuickOpenRecentAction } from 'vs/workbench/electron-browser/actions'; +import { CloseEditorAction, KeybindingsReferenceAction, OpenDocumentationUrlAction, OpenIntroductoryVideosUrlAction, OpenTipsAndTricksUrlAction, ReportIssueAction, ReportPerformanceIssueAction, ZoomResetAction, ZoomOutAction, ZoomInAction, ToggleFullScreenAction, ToggleMenuBarAction, CloseWorkspaceAction, CloseWindowAction, SwitchWindow, NewWindowAction, CloseMessagesAction, NavigateUpAction, NavigateDownAction, NavigateLeftAction, NavigateRightAction, IncreaseViewSizeAction, DecreaseViewSizeAction, ShowStartupPerformance, ToggleSharedProcessAction, QuickSwitchWindow, QuickOpenRecentAction } from 'vs/workbench/electron-browser/actions'; import { MessagesVisibleContext } from 'vs/workbench/electron-browser/workbench'; import { IJSONSchema } from 'vs/base/common/jsonSchema'; import { registerCommands } from 'vs/workbench/electron-browser/commands'; -import { AddRootFolderAction } from 'vs/workbench/browser/actions/fileActions'; +import { AddRootFolderAction, NewWorkspaceAction, OpenWorkspaceAction, SaveWorkspaceAction } from 'vs/workbench/browser/actions/workspaceActions'; // Contribute Commands registerCommands(); @@ -33,7 +33,7 @@ workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(CloseW workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(SwitchWindow, SwitchWindow.ID, SwitchWindow.LABEL, { primary: null, mac: { primary: KeyMod.WinCtrl | KeyCode.KEY_W } }), 'Switch Window...'); workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(QuickSwitchWindow, QuickSwitchWindow.ID, QuickSwitchWindow.LABEL), 'Quick Switch Window...'); workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(QuickOpenRecentAction, QuickOpenRecentAction.ID, QuickOpenRecentAction.LABEL), 'File: Quick Open Recent...', fileCategory); -workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(CloseFolderAction, CloseFolderAction.ID, CloseFolderAction.LABEL, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_F) }), 'File: Close Folder', fileCategory); +workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(CloseWorkspaceAction, CloseWorkspaceAction.ID, CloseWorkspaceAction.LABEL, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_F) }), 'File: Close Workspace', fileCategory); if (!!product.reportIssueUrl) { workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(ReportIssueAction, ReportIssueAction.ID, ReportIssueAction.LABEL), 'Help: Report Issues', helpCategory); workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(ReportPerformanceIssueAction, ReportPerformanceIssueAction.ID, ReportPerformanceIssueAction.LABEL), 'Help: Report Performance Issue', helpCategory); @@ -83,7 +83,11 @@ workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(Decrea // TODO@Ben multi root if (product.quality !== 'stable') { - workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(AddRootFolderAction, AddRootFolderAction.ID, AddRootFolderAction.LABEL), 'Files: Add Folder to Workspace...', fileCategory); + const workspacesCategory = nls.localize('workspaces', "Workspaces"); + workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(NewWorkspaceAction, NewWorkspaceAction.ID, NewWorkspaceAction.LABEL), 'Workspaces: New Workspace...', workspacesCategory); + workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(AddRootFolderAction, AddRootFolderAction.ID, AddRootFolderAction.LABEL), 'Workspaces: Add Folder to Workspace...', workspacesCategory); + workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(OpenWorkspaceAction, OpenWorkspaceAction.ID, OpenWorkspaceAction.LABEL), 'Workspaces: Open Workspace...', workspacesCategory); + workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(SaveWorkspaceAction, SaveWorkspaceAction.ID, SaveWorkspaceAction.LABEL), 'Workspaces: Save Workspace...', workspacesCategory); } // Developer related actions @@ -240,7 +244,7 @@ Note that there can still be cases where this setting is ignored (e.g. when usin nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'window.reopenFolders.none' }, "Never reopen a window. Always start with an empty one.") ], 'default': 'one', - 'description': nls.localize('restoreWindows', "Controls how windows are being reopened after a restart. Select 'none' to always start with an empty workspace, 'one' to reopen the last window you worked on, 'folders' to reopen all folders you had opened or 'all' to reopen all windows of your last session.") + 'description': nls.localize('restoreWindows', "Controls how windows are being reopened after a restart. Select 'none' to always start with an empty workspace, 'one' to reopen the last window you worked on, 'folders' to reopen all windows that had folders opened or 'all' to reopen all windows of your last session.") }, 'window.restoreFullscreen': { 'type': 'boolean', @@ -283,7 +287,7 @@ Note that there can still be cases where this setting is ignored (e.g. when usin 'window.closeWhenEmpty': { 'type': 'boolean', 'default': false, - 'description': nls.localize('closeWhenEmpty', "Controls if closing the last editor should also close the window. This setting only applies for windows that have no folder opened.") + 'description': nls.localize('closeWhenEmpty', "Controls if closing the last editor should also close the window. This setting only applies for windows that do not show folders.") } }; @@ -374,36 +378,4 @@ configurationRegistry.registerConfiguration({ 'description': nls.localize('zenMode.restore', "Controls if a window should restore to zen mode if it was exited in zen mode.") } } -}); - -// Configuration: Workspace -// TODO@Ben multi root -if (product.quality !== 'stable') { - configurationRegistry.registerConfiguration({ - 'id': 'workspace', - 'order': 10000, - 'title': nls.localize('workspaceConfigurationTitle', "Workspace"), - 'type': 'object', - 'properties': { - 'workspace': { - 'type': 'object', - 'description': nls.localize('workspaces.title', "Folder configuration of the workspace"), - 'additionalProperties': { - 'anyOf': [{ - 'type': 'object', - 'description': nls.localize('files.exclude.boolean', "The glob pattern to match file paths against. Set to true or false to enable or disable the pattern."), - 'properties': { - 'folders': { - 'description': nls.localize('workspaces.additionalFolders', "Folders of this workspace"), - 'type': 'array', - 'items': { - 'type': 'string' - } - } - } - }] - } - } - } - }); -} \ No newline at end of file +}); \ No newline at end of file diff --git a/src/vs/workbench/electron-browser/main.ts b/src/vs/workbench/electron-browser/main.ts index 3fe3ae5f165..9118720b026 100644 --- a/src/vs/workbench/electron-browser/main.ts +++ b/src/vs/workbench/electron-browser/main.ts @@ -19,8 +19,8 @@ import uri from 'vs/base/common/uri'; import strings = require('vs/base/common/strings'); import { IResourceInput } from 'vs/platform/editor/common/editor'; import { LegacyWorkspace, Workspace } from 'vs/platform/workspace/common/workspace'; -import { WorkspaceConfigurationService } from 'vs/workbench/services/configuration/node/configuration'; -import { realpath, stat } from 'vs/base/node/pfs'; +import { WorkspaceService, EmptyWorkspaceServiceImpl, WorkspaceServiceImpl } from 'vs/workbench/services/configuration/node/configuration'; +import { realpath } from 'vs/base/node/pfs'; import { EnvironmentService } from 'vs/platform/environment/node/environmentService'; import path = require('path'); import gracefulFs = require('graceful-fs'); @@ -35,7 +35,6 @@ import { StorageService, inMemoryLocalStorageInstance } from 'vs/platform/storag import { webFrame } from 'electron'; import fs = require('fs'); -import { createHash } from 'crypto'; gracefulFs.gracefulify(fs); // enable gracefulFs export function startup(configuration: IWindowConfiguration): TPromise { @@ -96,80 +95,70 @@ function toInputs(paths: IPath[], isUntitledFile?: boolean): IResourceInput[] { } function openWorkbench(configuration: IWindowConfiguration, options: IOptions): TPromise { - return resolveLegacyWorkspace(configuration).then(legacyWorkspace => { - const workspace = legacyWorkspaceToMultiRootWorkspace(legacyWorkspace); - const environmentService = new EnvironmentService(configuration, configuration.execPath); - const workspaceConfigurationService = new WorkspaceConfigurationService(environmentService, workspace); + const environmentService = new EnvironmentService(configuration, configuration.execPath); + + // Since the configuration service is one of the core services that is used in so many places, we initialize it + // right before startup of the workbench shell to have its data ready for consumers + return createAndInitializeWorkspaceService(configuration, environmentService).then(workspaceService => { + const workspace = workspaceService.getWorkspace(); + const legacyWorkspace = workspaceService.getLegacyWorkspace(); const timerService = new TimerService((window).MonacoEnvironment.timers as IInitData, !!workspace); const storageService = createStorageService(legacyWorkspace, workspace, configuration, environmentService); - // Since the configuration service is one of the core services that is used in so many places, we initialize it - // right before startup of the workbench shell to have its data ready for consumers - return workspaceConfigurationService.initialize().then(() => { - timerService.beforeDOMContentLoaded = Date.now(); + timerService.beforeDOMContentLoaded = Date.now(); - return domContentLoaded().then(() => { - timerService.afterDOMContentLoaded = Date.now(); + return domContentLoaded().then(() => { + timerService.afterDOMContentLoaded = Date.now(); - // Open Shell - timerService.beforeWorkbenchOpen = Date.now(); - const shell = new WorkbenchShell(document.body, { - contextService: workspaceConfigurationService, - configurationService: workspaceConfigurationService, - environmentService, - timerService, - storageService - }, configuration, options); - shell.open(); + // Open Shell + timerService.beforeWorkbenchOpen = Date.now(); + const shell = new WorkbenchShell(document.body, { + contextService: workspaceService, + configurationService: workspaceService, + environmentService, + timerService, + storageService + }, configuration, options); + shell.open(); - // Inform user about loading issues from the loader - (self).require.config({ - onError: (err: any) => { - if (err.errorCode === 'load') { - shell.onUnexpectedError(loaderError(err)); - } + // Inform user about loading issues from the loader + (self).require.config({ + onError: (err: any) => { + if (err.errorCode === 'load') { + shell.onUnexpectedError(loaderError(err)); } - }); + } }); }); }); } -function legacyWorkspaceToMultiRootWorkspace(legacyWorkspace: LegacyWorkspace): Workspace { - if (!legacyWorkspace) { - return null; - } +function createAndInitializeWorkspaceService(configuration: IWindowConfiguration, environmentService: EnvironmentService): TPromise { + return validateWorkspacePath(configuration).then(() => { + const workspaceConfigPath = configuration.workspace ? configuration.workspace.configPath : null; + const workspaceService = (workspaceConfigPath || configuration.folderPath) ? new WorkspaceServiceImpl(workspaceConfigPath, configuration.folderPath, environmentService) : new EmptyWorkspaceServiceImpl(environmentService); - return new Workspace( - createHash('md5').update(legacyWorkspace.resource.fsPath).update(legacyWorkspace.ctime ? String(legacyWorkspace.ctime) : '').digest('hex'), - path.basename(legacyWorkspace.resource.fsPath), - [legacyWorkspace.resource] - ); + return workspaceService.initialize().then(() => workspaceService, error => new EmptyWorkspaceServiceImpl(environmentService)); + }); } -function resolveLegacyWorkspace(configuration: IWindowConfiguration): TPromise { - if (!configuration.workspacePath) { +function validateWorkspacePath(configuration: IWindowConfiguration): TPromise { + if (!configuration.folderPath) { return TPromise.as(null); } - return realpath(configuration.workspacePath).then(realWorkspacePath => { + return realpath(configuration.folderPath).then(realFolderPath => { // for some weird reason, node adds a trailing slash to UNC paths // we never ever want trailing slashes as our workspace path unless // someone opens root ("/"). // See also https://github.com/nodejs/io.js/issues/1765 - if (paths.isUNC(realWorkspacePath) && strings.endsWith(realWorkspacePath, paths.nativeSep)) { - realWorkspacePath = strings.rtrim(realWorkspacePath, paths.nativeSep); + if (paths.isUNC(realFolderPath) && strings.endsWith(realFolderPath, paths.nativeSep)) { + realFolderPath = strings.rtrim(realFolderPath, paths.nativeSep); } // update config - configuration.workspacePath = realWorkspacePath; - - // resolve ctime of workspace - return stat(realWorkspacePath).then(folderStat => new LegacyWorkspace( - uri.file(realWorkspacePath), - platform.isLinux ? folderStat.ino : folderStat.birthtime.getTime() // On Linux, birthtime is ctime, so we cannot use it! We use the ino instead! - )); + configuration.folderPath = realFolderPath; }, error => { errors.onUnexpectedError(error); @@ -178,23 +167,39 @@ function resolveLegacyWorkspace(configuration: IWindowConfiguration): TPromise= 0 + ? 1000 * 60 * 60 * 24 * 7 // roughly 1 week + : 1000 * 60 * 60 * 24 * 30 * 3; // roughly 3 months + private _telemetryService: ITelemetryService; private _environmentService: IEnvironmentService; private _disposables: IDisposable[] = []; @@ -81,13 +86,11 @@ export class NodeCachedDataManager { readdir(nodeCachedDataDir).then(entries => { const now = Date.now(); - const limit = 1000 * 60 * 60 * 24 * 30 * 3; // roughly 3 months - const deletes = entries.map(entry => { const path = join(nodeCachedDataDir, entry); return stat(path).then(stats => { const diff = now - stats.mtime.getTime(); - if (diff > limit) { + if (diff > NodeCachedDataManager._DataMaxAge) { return rimraf(path); } return undefined; diff --git a/src/vs/workbench/electron-browser/shell.ts b/src/vs/workbench/electron-browser/shell.ts index e7d4031090e..850ce7021e7 100644 --- a/src/vs/workbench/electron-browser/shell.ts +++ b/src/vs/workbench/electron-browser/shell.ts @@ -52,6 +52,8 @@ import { IIntegrityService } from 'vs/platform/integrity/common/integrity'; import { EditorWorkerServiceImpl } from 'vs/editor/common/services/editorWorkerServiceImpl'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService'; import { MainProcessExtensionService } from 'vs/workbench/api/electron-browser/mainThreadExtensionService'; +import { IWorkspacesService } from 'vs/platform/workspaces/common/workspaces'; +import { WorkspacesChannelClient } from 'vs/platform/workspaces/common/workspacesIpc'; import { IOptions } from 'vs/workbench/common/options'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -384,6 +386,9 @@ export class WorkbenchShell { const urlChannel = mainProcessClient.getChannel('url'); serviceCollection.set(IURLService, new SyncDescriptor(URLChannelClient, urlChannel, currentWindow.id)); + const workspacesChannel = mainProcessClient.getChannel('workspaces'); + serviceCollection.set(IWorkspacesService, new SyncDescriptor(WorkspacesChannelClient, workspacesChannel)); + return [instantiationService, serviceCollection]; } diff --git a/src/vs/workbench/electron-browser/window.ts b/src/vs/workbench/electron-browser/window.ts index 25262058b5f..55b9526cc42 100644 --- a/src/vs/workbench/electron-browser/window.ts +++ b/src/vs/workbench/electron-browser/window.ts @@ -27,7 +27,7 @@ import { IEditorGroupService } from 'vs/workbench/services/group/common/groupSer import { IMessageService } from 'vs/platform/message/common/message'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IWorkspaceConfigurationService } from 'vs/workbench/services/configuration/common/configuration'; -import { IWindowsService, IWindowService, IWindowSettings, IWindowConfiguration, IPath, IOpenFileRequest } from 'vs/platform/windows/common/windows'; +import { IWindowsService, IWindowService, IWindowSettings, IPath, IOpenFileRequest, IWindowsConfiguration } from 'vs/platform/windows/common/windows'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; @@ -304,7 +304,7 @@ export class ElectronWindow extends Themable { // Configuration changes let previousConfiguredZoomLevel: number; this.configurationService.onDidUpdateConfiguration(e => { - const windowConfig: IWindowConfiguration = this.configurationService.getConfiguration(); + const windowConfig: IWindowsConfiguration = this.configurationService.getConfiguration(); let newZoomLevel = 0; if (windowConfig.window && typeof windowConfig.window.zoomLevel === 'number') { diff --git a/src/vs/workbench/electron-browser/workbench.ts b/src/vs/workbench/electron-browser/workbench.ts index a56a3bc70e5..379555a89bb 100644 --- a/src/vs/workbench/electron-browser/workbench.ts +++ b/src/vs/workbench/electron-browser/workbench.ts @@ -47,9 +47,11 @@ import { IStorageService, StorageScope } from 'vs/platform/storage/common/storag import { ContextMenuService } from 'vs/workbench/services/contextview/electron-browser/contextmenuService'; import { WorkbenchKeybindingService } from 'vs/workbench/services/keybinding/electron-browser/keybindingService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { WorkspaceConfigurationService } from 'vs/workbench/services/configuration/node/configuration'; +import { WorkspaceService } from 'vs/workbench/services/configuration/node/configuration'; import { IConfigurationEditingService } from 'vs/workbench/services/configuration/common/configurationEditing'; import { ConfigurationEditingService } from 'vs/workbench/services/configuration/node/configurationEditingService'; +import { IJSONEditingService } from 'vs/workbench/services/configuration/common/jsonEditing'; +import { JSONEditingService } from 'vs/workbench/services/configuration/node/jsonEditingService'; import { ContextKeyService } from 'vs/platform/contextkey/browser/contextKeyService'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IKeybindingEditingService, KeybindingsEditingService } from 'vs/workbench/services/keybinding/common/keybindingEditing'; @@ -219,7 +221,7 @@ export class Workbench implements IPartService { @IStorageService private storageService: IStorageService, @ILifecycleService private lifecycleService: ILifecycleService, @IMessageService private messageService: IMessageService, - @IConfigurationService private configurationService: WorkspaceConfigurationService, + @IConfigurationService private configurationService: WorkspaceService, @ITelemetryService private telemetryService: ITelemetryService, @IEnvironmentService private environmentService: IEnvironmentService, @IWindowService private windowService: IWindowService @@ -349,10 +351,12 @@ export class Workbench implements IPartService { editorRestoreStopWatch.stop(); for (const editor of editors) { - if (editor && editor.input) { - restoredEditors.push(editor.input.getName()); - } else { - restoredEditors.push(`other:${editor.getId()}`); + if (editor) { + if (editor.input) { + restoredEditors.push(editor.input.getName()); + } else { + restoredEditors.push(`other:${editor.getId()}`); + } } } }); @@ -582,6 +586,10 @@ export class Workbench implements IPartService { // Text Model Resolver Service serviceCollection.set(ITextModelService, new SyncDescriptor(TextModelResolverService)); + // JSON Editing + const jsonEditingService = this.instantiationService.createInstance(JSONEditingService); + serviceCollection.set(IJSONEditingService, jsonEditingService); + // Configuration Editing this.configurationEditingService = this.instantiationService.createInstance(ConfigurationEditingService); serviceCollection.set(IConfigurationEditingService, this.configurationEditingService); diff --git a/src/vs/workbench/parts/codeEditor/electron-browser/accessibility.ts b/src/vs/workbench/parts/codeEditor/electron-browser/accessibility.ts index a88b7a7d354..ddbc5ae45e6 100644 --- a/src/vs/workbench/parts/codeEditor/electron-browser/accessibility.ts +++ b/src/vs/workbench/parts/codeEditor/electron-browser/accessibility.ts @@ -253,6 +253,8 @@ class AccessibilityHelpWidget extends Widget implements IOverlayWidget { text += '\n\n' + nls.localize('outroMsg', "You can dismiss this tooltip and return to the editor by pressing Escape or Shift+Escape."); this._contentDomNode.domNode.appendChild(renderFormattedText(text)); + // Per https://www.w3.org/TR/wai-aria/roles#document, Authors SHOULD provide a title or label for documents + this._contentDomNode.domNode.setAttribute('aria-label', text); } public hide(): void { diff --git a/src/vs/workbench/parts/debug/electron-browser/statusbarColorProvider.ts b/src/vs/workbench/parts/debug/electron-browser/statusbarColorProvider.ts index 78ba083f9a0..a47fa6440ac 100644 --- a/src/vs/workbench/parts/debug/electron-browser/statusbarColorProvider.ts +++ b/src/vs/workbench/parts/debug/electron-browser/statusbarColorProvider.ts @@ -10,7 +10,7 @@ import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { IPartService, Parts } from 'vs/workbench/services/part/common/partService'; import { IDebugService, State } from 'vs/workbench/parts/debug/common/debug'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { STATUS_BAR_NO_FOLDER_BACKGROUND, STATUS_BAR_NO_FOLDER_FOREGROUND, STATUS_BAR_BACKGROUND, Themable, STATUS_BAR_FOREGROUND } from 'vs/workbench/common/theme'; +import { STATUS_BAR_NO_FOLDER_BACKGROUND, STATUS_BAR_MULTI_FOLDER_BACKGROUND, STATUS_BAR_MULTI_FOLDER_FOREGROUND, STATUS_BAR_NO_FOLDER_FOREGROUND, STATUS_BAR_BACKGROUND, Themable, STATUS_BAR_FOREGROUND } from 'vs/workbench/common/theme'; import { addClass, removeClass } from 'vs/base/browser/dom'; // colors for theming @@ -59,14 +59,18 @@ export class StatusBarColorProvider extends Themable implements IWorkbenchContri removeClass(container, 'debugging'); } - container.style.backgroundColor = this.getColor(this.getColorKey(STATUS_BAR_NO_FOLDER_BACKGROUND, STATUS_BAR_DEBUGGING_BACKGROUND, STATUS_BAR_BACKGROUND)); - container.style.color = this.getColor(this.getColorKey(STATUS_BAR_NO_FOLDER_FOREGROUND, STATUS_BAR_DEBUGGING_FOREGROUND, STATUS_BAR_FOREGROUND)); + container.style.backgroundColor = this.getColor(this.getColorKey(STATUS_BAR_MULTI_FOLDER_BACKGROUND, STATUS_BAR_NO_FOLDER_BACKGROUND, STATUS_BAR_DEBUGGING_BACKGROUND, STATUS_BAR_BACKGROUND)); + container.style.color = this.getColor(this.getColorKey(STATUS_BAR_MULTI_FOLDER_FOREGROUND, STATUS_BAR_NO_FOLDER_FOREGROUND, STATUS_BAR_DEBUGGING_FOREGROUND, STATUS_BAR_FOREGROUND)); } - private getColorKey(noFolderColor: string, debuggingColor: string, normalColor: string): string { + private getColorKey(multiFolderColor, noFolderColor: string, debuggingColor: string, normalColor: string): string { // Not debugging if (!this.isDebugging()) { + if (this.contextService.hasMultiFolderWorkspace()) { + return multiFolderColor; + } + if (this.contextService.hasWorkspace()) { return normalColor; } diff --git a/src/vs/workbench/parts/execution/electron-browser/execution.contribution.ts b/src/vs/workbench/parts/execution/electron-browser/execution.contribution.ts index d0efdbaa2ae..1f8d65165b1 100644 --- a/src/vs/workbench/parts/execution/electron-browser/execution.contribution.ts +++ b/src/vs/workbench/parts/execution/electron-browser/execution.contribution.ts @@ -4,10 +4,30 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; +import * as nls from 'vs/nls'; import * as env from 'vs/base/common/platform'; -import { WinTerminalService, MacTerminalService, LinuxTerminalService } from 'vs/workbench/parts/execution/electron-browser/terminalService'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { IAction, Action } from 'vs/base/common/actions'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actionRegistry'; +import paths = require('vs/base/common/paths'); +import { Scope, IActionBarRegistry, Extensions as ActionBarExtensions, ActionBarContributor } from 'vs/workbench/browser/actions'; +import uri from 'vs/base/common/uri'; +import { explorerItemToFileResource } from 'vs/workbench/parts/files/common/files'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { ITerminalService } from 'vs/workbench/parts/execution/common/execution'; +import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { toResource } from 'vs/workbench/common/editor'; +import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; +import { Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; +import { ITerminalService as IIntegratedTerminalService, KEYBINDING_CONTEXT_TERMINAL_NOT_FOCUSED } from 'vs/workbench/parts/terminal/common/terminal'; +import { DEFAULT_TERMINAL_WINDOWS, DEFAULT_TERMINAL_LINUX_READY, DEFAULT_TERMINAL_OSX, ITerminalConfiguration } from 'vs/workbench/parts/execution/electron-browser/terminal'; +import { WinTerminalService, MacTerminalService, LinuxTerminalService } from 'vs/workbench/parts/execution/electron-browser/terminalService'; +import { IHistoryService } from 'vs/workbench/services/history/common/history'; if (env.isWindows) { registerSingleton(ITerminalService, WinTerminalService); @@ -16,3 +36,192 @@ if (env.isWindows) { } else if (env.isLinux) { registerSingleton(ITerminalService, LinuxTerminalService); } + +DEFAULT_TERMINAL_LINUX_READY.then(defaultTerminalLinux => { + let configurationRegistry = Registry.as(Extensions.Configuration); + configurationRegistry.registerConfiguration({ + 'id': 'externalTerminal', + 'order': 100, + 'title': nls.localize('terminalConfigurationTitle', "External Terminal"), + 'type': 'object', + 'properties': { + 'terminal.explorerKind': { + 'type': 'string', + 'enum': [ + 'integrated', + 'external' + ], + 'description': nls.localize('explorer.openInTerminalKind', "Customizes what kind of terminal to launch."), + 'default': 'integrated', + 'isExecutable': false + }, + 'terminal.external.windowsExec': { + 'type': 'string', + 'description': nls.localize('terminal.external.windowsExec', "Customizes which terminal to run on Windows."), + 'default': DEFAULT_TERMINAL_WINDOWS, + 'isExecutable': true + }, + 'terminal.external.osxExec': { + 'type': 'string', + 'description': nls.localize('terminal.external.osxExec', "Customizes which terminal application to run on OS X."), + 'default': DEFAULT_TERMINAL_OSX, + 'isExecutable': true + }, + 'terminal.external.linuxExec': { + 'type': 'string', + 'description': nls.localize('terminal.external.linuxExec', "Customizes which terminal to run on Linux."), + 'default': defaultTerminalLinux, + 'isExecutable': true + } + } + }); +}); + + +export abstract class AbstractOpenInTerminalAction extends Action { + private resource: uri; + + constructor( + id: string, + label: string, + @IWorkbenchEditorService protected editorService: IWorkbenchEditorService, + @IWorkspaceContextService protected contextService: IWorkspaceContextService, + @IHistoryService protected historyService: IHistoryService + ) { + super(id, label); + + this.order = 49; // Allow other actions to position before or after + } + + public setResource(resource: uri): void { + this.resource = resource; + this.enabled = !paths.isUNC(this.resource.fsPath); + } + + public getPathToOpen(): string { + let pathToOpen: string; + + // Try workspace path first + const root = this.historyService.getLastActiveWorkspaceRoot(); + pathToOpen = this.resource ? this.resource.fsPath : (root && root.fsPath); + + // Otherwise check if we have an active file open + if (!pathToOpen) { + const file = toResource(this.editorService.getActiveEditorInput(), { supportSideBySide: true, filter: 'file' }); + if (file) { + pathToOpen = paths.dirname(file.fsPath); // take parent folder of file + } + } + + return pathToOpen; + } +} + +export class OpenConsoleAction extends AbstractOpenInTerminalAction { + + public static ID = 'workbench.action.terminal.openNativeConsole'; + public static Label = env.isWindows ? nls.localize('globalConsoleActionWin', "Open New Command Prompt") : + nls.localize('globalConsoleActionMacLinux', "Open New Terminal"); + public static ScopedLabel = env.isWindows ? nls.localize('scopedConsoleActionWin', "Open in Command Prompt") : + nls.localize('scopedConsoleActionMacLinux', "Open in Terminal"); + + constructor( + id: string, + label: string, + @ITerminalService private terminalService: ITerminalService, + @IWorkbenchEditorService editorService: IWorkbenchEditorService, + @IWorkspaceContextService contextService: IWorkspaceContextService, + @IHistoryService historyService: IHistoryService + ) { + super(id, label, editorService, contextService, historyService); + } + + public run(event?: any): TPromise { + let pathToOpen = this.getPathToOpen(); + this.terminalService.openTerminal(pathToOpen); + + return TPromise.as(null); + } +} + +export class OpenIntegratedTerminalAction extends AbstractOpenInTerminalAction { + + public static ID = 'workbench.action.terminal.openFolderInIntegratedTerminal'; + public static Label = nls.localize('openFolderInIntegratedTerminal', "Open in Terminal"); + + constructor( + id: string, + label: string, + @IIntegratedTerminalService private integratedTerminalService: IIntegratedTerminalService, + @IWorkbenchEditorService editorService: IWorkbenchEditorService, + @IWorkspaceContextService contextService: IWorkspaceContextService, + @IHistoryService historyService: IHistoryService + ) { + super(id, label, editorService, contextService, historyService); + } + + public run(event?: any): TPromise { + let pathToOpen = this.getPathToOpen(); + + var instance = this.integratedTerminalService.createInstance({ cwd: pathToOpen }, true); + if (instance) { + this.integratedTerminalService.setActiveInstance(instance); + this.integratedTerminalService.showPanel(true); + } + return TPromise.as(null); + } +} + +export class ExplorerViewerActionContributor extends ActionBarContributor { + + constructor( + @IInstantiationService private instantiationService: IInstantiationService, + @IConfigurationService private configurationService: IConfigurationService + ) { + super(); + } + + public hasSecondaryActions(context: any): boolean { + return !!explorerItemToFileResource(context.element); + } + + public getSecondaryActions(context: any): IAction[] { + let fileResource = explorerItemToFileResource(context.element); + let resource = fileResource.resource; + + // We want the parent unless this resource is a directory + if (!fileResource.isDirectory) { + resource = uri.file(paths.dirname(resource.fsPath)); + } + + const configuration = this.configurationService.getConfiguration(); + const explorerKind = configuration.terminal.explorerKind; + + if (explorerKind === 'integrated') { + let action = this.instantiationService.createInstance(OpenIntegratedTerminalAction, OpenIntegratedTerminalAction.ID, OpenIntegratedTerminalAction.Label); + action.setResource(resource); + + return [action]; + } else { + let action = this.instantiationService.createInstance(OpenConsoleAction, OpenConsoleAction.ID, OpenConsoleAction.ScopedLabel); + action.setResource(resource); + + return [action]; + } + } +} + +const actionBarRegistry = Registry.as(ActionBarExtensions.Actionbar); +actionBarRegistry.registerActionBarContributor(Scope.VIEWER, ExplorerViewerActionContributor); + +// Register Global Action to Open Console +Registry.as(ActionExtensions.WorkbenchActions).registerWorkbenchAction( + new SyncActionDescriptor( + OpenConsoleAction, + OpenConsoleAction.ID, + OpenConsoleAction.Label, + { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_C }, + KEYBINDING_CONTEXT_TERMINAL_NOT_FOCUSED + ), + env.isWindows ? 'Open New Command Prompt' : 'Open New Terminal' +); diff --git a/src/vs/workbench/parts/execution/electron-browser/terminal.contribution.ts b/src/vs/workbench/parts/execution/electron-browser/terminal.contribution.ts deleted file mode 100644 index 0dd513654d2..00000000000 --- a/src/vs/workbench/parts/execution/electron-browser/terminal.contribution.ts +++ /dev/null @@ -1,147 +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 nls = require('vs/nls'); -import { TPromise } from 'vs/base/common/winjs.base'; -import { Registry } from 'vs/platform/registry/common/platform'; -import baseplatform = require('vs/base/common/platform'); -import { IAction, Action } from 'vs/base/common/actions'; -import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actionRegistry'; -import paths = require('vs/base/common/paths'); -import { Scope, IActionBarRegistry, Extensions as ActionBarExtensions, ActionBarContributor } from 'vs/workbench/browser/actions'; -import uri from 'vs/base/common/uri'; -import { explorerItemToFileResource } from 'vs/workbench/parts/files/common/files'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { ITerminalService } from 'vs/workbench/parts/execution/common/execution'; -import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { toResource } from 'vs/workbench/common/editor'; -import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; -import { Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; -import { KEYBINDING_CONTEXT_TERMINAL_NOT_FOCUSED } from 'vs/workbench/parts/terminal/common/terminal'; -import { DEFAULT_TERMINAL_WINDOWS, DEFAULT_TERMINAL_LINUX_READY, DEFAULT_TERMINAL_OSX } from 'vs/workbench/parts/execution/electron-browser/terminal'; -import { IHistoryService } from 'vs/workbench/services/history/common/history'; - -DEFAULT_TERMINAL_LINUX_READY.then(defaultTerminalLinux => { - let configurationRegistry = Registry.as(Extensions.Configuration); - configurationRegistry.registerConfiguration({ - 'id': 'externalTerminal', - 'order': 100, - 'title': nls.localize('terminalConfigurationTitle', "External Terminal"), - 'type': 'object', - 'properties': { - 'terminal.external.windowsExec': { - 'type': 'string', - 'description': nls.localize('terminal.external.windowsExec', "Customizes which terminal to run on Windows."), - 'default': DEFAULT_TERMINAL_WINDOWS, - 'isExecutable': true - }, - 'terminal.external.osxExec': { - 'type': 'string', - 'description': nls.localize('terminal.external.osxExec', "Customizes which terminal application to run on OS X."), - 'default': DEFAULT_TERMINAL_OSX, - 'isExecutable': true - }, - 'terminal.external.linuxExec': { - 'type': 'string', - 'description': nls.localize('terminal.external.linuxExec', "Customizes which terminal to run on Linux."), - 'default': defaultTerminalLinux, - 'isExecutable': true - } - } - }); -}); - -export class OpenConsoleAction extends Action { - - public static ID = 'workbench.action.terminal.openNativeConsole'; - public static Label = baseplatform.isWindows ? nls.localize('globalConsoleActionWin', "Open New Command Prompt") : - nls.localize('globalConsoleActionMacLinux', "Open New Terminal"); - public static ScopedLabel = baseplatform.isWindows ? nls.localize('scopedConsoleActionWin', "Open in Command Prompt") : - nls.localize('scopedConsoleActionMacLinux', "Open in Terminal"); - - private resource: uri; - - constructor( - id: string, - label: string, - @ITerminalService private terminalService: ITerminalService, - @IWorkbenchEditorService private editorService: IWorkbenchEditorService, - @IWorkspaceContextService private contextService: IWorkspaceContextService, - @IHistoryService private historyService: IHistoryService - ) { - super(id, label); - - this.order = 49; // Allow other actions to position before or after - } - - public setResource(resource: uri): void { - this.resource = resource; - this.enabled = !paths.isUNC(this.resource.fsPath); - } - - public run(event?: any): TPromise { - let pathToOpen: string; - - // Try workspace path first - const root = this.historyService.getLastActiveWorkspaceRoot(); - pathToOpen = this.resource ? this.resource.fsPath : (root && root.fsPath); - - // Otherwise check if we have an active file open - if (!pathToOpen) { - const file = toResource(this.editorService.getActiveEditorInput(), { supportSideBySide: true, filter: 'file' }); - if (file) { - pathToOpen = paths.dirname(file.fsPath); // take parent folder of file - } - } - - this.terminalService.openTerminal(pathToOpen); - - return TPromise.as(null); - } -} - -class ExplorerViewerActionContributor extends ActionBarContributor { - - constructor( @IInstantiationService private instantiationService: IInstantiationService) { - super(); - } - - public hasSecondaryActions(context: any): boolean { - return !!explorerItemToFileResource(context.element); - } - - public getSecondaryActions(context: any): IAction[] { - let fileResource = explorerItemToFileResource(context.element); - let resource = fileResource.resource; - - // We want the parent unless this resource is a directory - if (!fileResource.isDirectory) { - resource = uri.file(paths.dirname(resource.fsPath)); - } - - let action = this.instantiationService.createInstance(OpenConsoleAction, OpenConsoleAction.ID, OpenConsoleAction.ScopedLabel); - action.setResource(resource); - - return [action]; - } -} - -const actionBarRegistry = Registry.as(ActionBarExtensions.Actionbar); -actionBarRegistry.registerActionBarContributor(Scope.VIEWER, ExplorerViewerActionContributor); - -// Register Global Action to Open Console -Registry.as(ActionExtensions.WorkbenchActions).registerWorkbenchAction( - new SyncActionDescriptor( - OpenConsoleAction, - OpenConsoleAction.ID, - OpenConsoleAction.Label, - { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_C }, - KEYBINDING_CONTEXT_TERMINAL_NOT_FOCUSED - ), - baseplatform.isWindows ? 'Open New Command Prompt' : 'Open New Terminal' -); diff --git a/src/vs/workbench/parts/execution/electron-browser/terminal.ts b/src/vs/workbench/parts/execution/electron-browser/terminal.ts index be37fdd4c37..9ddb4c6369e 100644 --- a/src/vs/workbench/parts/execution/electron-browser/terminal.ts +++ b/src/vs/workbench/parts/execution/electron-browser/terminal.ts @@ -37,6 +37,7 @@ export const DEFAULT_TERMINAL_WINDOWS = `${process.env.windir}\\${process.env.ha export interface ITerminalConfiguration { terminal: { + explorerKind: 'integrated' | 'external', external: { linuxExec: string, osxExec: string, diff --git a/src/vs/workbench/parts/execution/test/electron-browser/terminalService.test.ts b/src/vs/workbench/parts/execution/test/electron-browser/terminalService.test.ts index 7b66a23d27e..e26b830c9dd 100644 --- a/src/vs/workbench/parts/execution/test/electron-browser/terminalService.test.ts +++ b/src/vs/workbench/parts/execution/test/electron-browser/terminalService.test.ts @@ -17,6 +17,7 @@ suite('Execution - TerminalService', () => { setup(() => { mockConfig = { terminal: { + explorerKind: 'external', external: { windowsExec: 'testWindowsShell', osxExec: 'testOSXShell', diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensionsViewlet.ts b/src/vs/workbench/parts/extensions/electron-browser/extensionsViewlet.ts index 4f8e99d0488..a8075bdf6df 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/extensionsViewlet.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/extensionsViewlet.ts @@ -57,6 +57,7 @@ interface SearchInputEvent extends Event { immediate?: boolean; } +const ExtensionsViewletVisibleContext = new RawContextKey('extensionsViewletVisible', false); const SearchExtensionsContext = new RawContextKey('searchExtensions', false); const SearchInstalledExtensionsContext = new RawContextKey('searchInstalledExtensions', false); const SearchRecommendedExtensionsContext = new RawContextKey('searchRecommendedExtensions', false); @@ -64,6 +65,7 @@ const SearchRecommendedExtensionsContext = new RawContextKey('searchRec export class ExtensionsViewlet extends ComposedViewsViewlet implements IExtensionsViewlet { private onSearchChange: EventOf; + private extensionsViewletVisibleContextKey: IContextKey; private searchExtensionsContextKey: IContextKey; private searchInstalledExtensionsContextKey: IContextKey; private searchRecommendedExtensionsContextKey: IContextKey; @@ -101,6 +103,7 @@ export class ExtensionsViewlet extends ComposedViewsViewlet implements IExtensio this.registerViews(); this.searchDelayer = new ThrottledDelayer(500); + this.extensionsViewletVisibleContextKey = ExtensionsViewletVisibleContext.bindTo(contextKeyService); this.searchExtensionsContextKey = SearchExtensionsContext.bindTo(contextKeyService); this.searchInstalledExtensionsContextKey = SearchInstalledExtensionsContext.bindTo(contextKeyService); this.searchRecommendedExtensionsContextKey = SearchRecommendedExtensionsContext.bindTo(contextKeyService); @@ -133,7 +136,7 @@ export class ExtensionsViewlet extends ComposedViewsViewlet implements IExtensio name: localize('marketPlace', "Marketplace"), location: ViewLocation.Extensions, ctor: ExtensionsListView, - when: ContextKeyExpr.and(ContextKeyExpr.has('searchExtensions'), ContextKeyExpr.not('searchInstalledExtensions')), + when: ContextKeyExpr.and(ContextKeyExpr.has('extensionsViewletVisible'), ContextKeyExpr.has('searchExtensions'), ContextKeyExpr.not('searchInstalledExtensions')), size: 100 }; } @@ -144,7 +147,7 @@ export class ExtensionsViewlet extends ComposedViewsViewlet implements IExtensio name: localize('installedExtensions', "Installed"), location: ViewLocation.Extensions, ctor: InstalledExtensionsView, - when: ContextKeyExpr.not('searchExtensions'), + when: ContextKeyExpr.and(ContextKeyExpr.has('extensionsViewletVisible'), ContextKeyExpr.not('searchExtensions')), size: 50 }; } @@ -155,7 +158,7 @@ export class ExtensionsViewlet extends ComposedViewsViewlet implements IExtensio name: localize('searchInstalledExtensions', "Installed"), location: ViewLocation.Extensions, ctor: InstalledExtensionsView, - when: ContextKeyExpr.has('searchInstalledExtensions'), + when: ContextKeyExpr.and(ContextKeyExpr.has('extensionsViewletVisible'), ContextKeyExpr.has('searchInstalledExtensions')), size: 50 }; } @@ -166,7 +169,7 @@ export class ExtensionsViewlet extends ComposedViewsViewlet implements IExtensio name: localize('recommendedExtensions', "Recommended"), location: ViewLocation.Extensions, ctor: RecommendedExtensionsView, - when: ContextKeyExpr.not('searchExtensions'), + when: ContextKeyExpr.and(ContextKeyExpr.has('extensionsViewletVisible'), ContextKeyExpr.not('searchExtensions')), size: 50, canToggleVisibility: true }; @@ -225,16 +228,12 @@ export class ExtensionsViewlet extends ComposedViewsViewlet implements IExtensio setVisible(visible: boolean): TPromise { const isVisibilityChanged = this.isVisible() !== visible; - if (isVisibilityChanged) { - if (visible) { - this.searchBox.focus(); - this.searchBox.setSelectionRange(0, this.searchBox.value.length); - } - } return super.setVisible(visible).then(() => { if (isVisibilityChanged) { + this.extensionsViewletVisibleContextKey.set(visible); if (visible) { - this.doSearch(); + this.searchBox.focus(); + this.searchBox.setSelectionRange(0, this.searchBox.value.length); } } }); diff --git a/src/vs/workbench/parts/files/browser/fileActions.contribution.ts b/src/vs/workbench/parts/files/browser/fileActions.contribution.ts index a0dad3c1940..bba1d4a439a 100644 --- a/src/vs/workbench/parts/files/browser/fileActions.contribution.ts +++ b/src/vs/workbench/parts/files/browser/fileActions.contribution.ts @@ -19,7 +19,7 @@ import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { FileStat, Model } from 'vs/workbench/parts/files/common/explorerModel'; import { KeyMod, KeyChord, KeyCode } from 'vs/base/common/keyCodes'; -import { OpenFolderAction, OpenFileFolderAction, AddRootFolderAction, RemoveRootFolderAction } from 'vs/workbench/browser/actions/fileActions'; +import { OpenFolderAction, OpenFileFolderAction, AddRootFolderAction, RemoveRootFolderAction } from 'vs/workbench/browser/actions/workspaceActions'; import { copyFocusedFilesExplorerViewItem, revealInOSFocusedFilesExplorerItem, openFocusedExplorerItemSideBySideCommand, copyPathOfFocusedExplorerItem, copyPathCommand, revealInExplorerCommand, revealInOSCommand, openFolderPickerCommand, openWindowCommand, openFileInNewWindowCommand, deleteFocusedFilesExplorerViewItemCommand, moveFocusedFilesExplorerViewItemToTrashCommand, renameFocusedFilesExplorerViewItemCommand } from 'vs/workbench/parts/files/browser/fileCommands'; import { CommandsRegistry, ICommandHandler } from 'vs/platform/commands/common/commands'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; diff --git a/src/vs/workbench/parts/files/browser/views/emptyView.ts b/src/vs/workbench/parts/files/browser/views/emptyView.ts index d12878eeb73..10afaa7187b 100644 --- a/src/vs/workbench/parts/files/browser/views/emptyView.ts +++ b/src/vs/workbench/parts/files/browser/views/emptyView.ts @@ -15,7 +15,7 @@ import { $ } from 'vs/base/browser/builder'; import { IActionItem } from 'vs/base/browser/ui/actionbar/actionbar'; import { CollapsibleView, IViewletViewOptions, IViewOptions } from 'vs/workbench/parts/views/browser/views'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { OpenFolderAction, OpenFileFolderAction } from 'vs/workbench/browser/actions/fileActions'; +import { OpenFolderAction, OpenFileFolderAction } from 'vs/workbench/browser/actions/workspaceActions'; import { attachButtonStyler } from 'vs/platform/theme/common/styler'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; diff --git a/src/vs/workbench/parts/files/browser/views/explorerView.ts b/src/vs/workbench/parts/files/browser/views/explorerView.ts index 357105eb10a..3d900ba57fb 100644 --- a/src/vs/workbench/parts/files/browser/views/explorerView.ts +++ b/src/vs/workbench/parts/files/browser/views/explorerView.ts @@ -142,7 +142,8 @@ export class ExplorerView extends CollapsibleView { } public get name(): string { - return nls.localize('folders', "Folders"); + const workspace = this.contextService.getWorkspace(); + return workspace.roots.length === 1 ? workspace.name : nls.localize('folders', "Folders"); } public set name(value) { @@ -726,8 +727,6 @@ export class ExplorerView extends CollapsibleView { let targetsToExpand: URI[] = []; if (this.settings[ExplorerView.MEMENTO_EXPANDED_FOLDER_RESOURCES]) { targetsToExpand = this.settings[ExplorerView.MEMENTO_EXPANDED_FOLDER_RESOURCES].map((e: string) => URI.parse(e)); - } else if (this.model.roots.length === 1) { - targetsToExpand = this.model.roots.map(root => root.resource); // always expand single root folder } // First time refresh: Receive target through active editor input or selection and also include settings from previous session @@ -777,7 +776,7 @@ export class ExplorerView extends CollapsibleView { // Subsequent refresh: Merge stat into our local model and refresh tree modelStats.forEach((modelStat, index) => FileStat.mergeLocalWithDisk(modelStat, this.model.roots[index])); - const input = this.model; + const input = this.model.roots.length === 1 ? this.model.roots[0] : this.model; if (input === this.explorerViewer.getInput()) { return this.explorerViewer.refresh(); } @@ -788,6 +787,7 @@ export class ExplorerView extends CollapsibleView { const statsToExpand = expanded.length ? [this.model.roots[0]].concat(expanded) : targetsToExpand.map(expand => this.model.findClosest(expand)); + // Display roots only when there is more than 1 root // Make sure to expand all folders that where expanded in the previous session return this.explorerViewer.setInput(input).then(() => this.explorerViewer.expandAll(statsToExpand)); }, e => TPromise.wrapError(e)); diff --git a/src/vs/workbench/parts/files/browser/views/explorerViewer.ts b/src/vs/workbench/parts/files/browser/views/explorerViewer.ts index 3eef9e6afc9..879eb83ef14 100644 --- a/src/vs/workbench/parts/files/browser/views/explorerViewer.ts +++ b/src/vs/workbench/parts/files/browser/views/explorerViewer.ts @@ -560,7 +560,6 @@ export class FileSorter implements ISorter { // Sort Directories switch (this.sortOrder) { case 'default': - case 'type': case 'modified': if (statA.isDirectory && !statB.isDirectory) { return -1; @@ -572,6 +571,21 @@ export class FileSorter implements ISorter { break; + case 'type': + if (statA.isDirectory && !statB.isDirectory) { + return -1; + } + + if (statB.isDirectory && !statA.isDirectory) { + return 1; + } + + if (statA.isDirectory && statB.isDirectory) { + return comparers.compareFileNames(statA.name, statB.name); + } + + break; + case 'filesFirst': if (statA.isDirectory && !statB.isDirectory) { return 1; diff --git a/src/vs/workbench/parts/html/browser/html.contribution.ts b/src/vs/workbench/parts/html/browser/html.contribution.ts index fd782f55d8f..ddb472e6a37 100644 --- a/src/vs/workbench/parts/html/browser/html.contribution.ts +++ b/src/vs/workbench/parts/html/browser/html.contribution.ts @@ -18,6 +18,8 @@ import { EditorDescriptor } from 'vs/workbench/browser/parts/editor/baseEditor'; import { IEditorRegistry, Extensions as EditorExtensions } from 'vs/workbench/common/editor'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService'; +import { MenuRegistry } from "vs/platform/actions/common/actions"; +import { WebviewElement } from "vs/workbench/parts/html/browser/webview"; // --- Register Editor (Registry.as(EditorExtensions.Editors)).registerEditor(new EditorDescriptor(HtmlPreviewPart.ID, @@ -78,3 +80,20 @@ CommandsRegistry.registerCommand('_workbench.htmlPreview.postMessage', (accessor } return activePreviews.length > 0; }); + + +CommandsRegistry.registerCommand('_webview.openDevTools', function () { + const elements = document.querySelectorAll('webview.ready'); + for (let i = 0; i < elements.length; i++) { + try { + (elements.item(i) as WebviewElement).openDevTools(); + } catch (e) { + console.error(e); + } + } +}); + +MenuRegistry.addCommand({ + id: '_webview.openDevTools', + title: localize('devtools.webview', "Developer: Webview Tools") +}); diff --git a/src/vs/workbench/parts/html/browser/htmlPreviewPart.ts b/src/vs/workbench/parts/html/browser/htmlPreviewPart.ts index ae22f63292b..3d956d9044f 100644 --- a/src/vs/workbench/parts/html/browser/htmlPreviewPart.ts +++ b/src/vs/workbench/parts/html/browser/htmlPreviewPart.ts @@ -97,7 +97,6 @@ export class HtmlPreviewPart extends WebviewEditor { this._webviewDisposables = [ this._webview, this._webview.onDidClickLink(uri => this.openerService.open(uri)), - this._webview.onDidLoadContent(data => this.telemetryService.publicLog('previewHtml', data.stats)), this._webview.onDidScroll(data => { this.scrollYPercentage = data.scrollYPercentage; }), diff --git a/src/vs/workbench/parts/html/browser/webview.ts b/src/vs/workbench/parts/html/browser/webview.ts index b68d7995459..59d06e85c47 100644 --- a/src/vs/workbench/parts/html/browser/webview.ts +++ b/src/vs/workbench/parts/html/browser/webview.ts @@ -5,25 +5,20 @@ 'use strict'; -import { localize } from 'vs/nls'; import URI from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import Event, { Emitter } from 'vs/base/common/event'; import { addDisposableListener, addClass } from 'vs/base/browser/dom'; -import { CommandsRegistry } from 'vs/platform/commands/common/commands'; -import { MenuRegistry } from 'vs/platform/actions/common/actions'; import { editorBackground, editorForeground } from 'vs/platform/theme/common/colorRegistry'; import { ITheme, LIGHT, DARK } from 'vs/platform/theme/common/themeService'; import { WebviewFindWidget } from './webviewFindWidget'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IContextKey } from 'vs/platform/contextkey/common/contextkey'; -declare interface WebviewElement extends HTMLElement { +export declare interface WebviewElement extends HTMLElement { src: string; - autoSize: 'on'; preload: string; - contextIsolation: boolean; send(channel: string, ...args: any[]); openDevTools(): any; getWebContents(): any; @@ -52,22 +47,6 @@ export interface FoundInPageResults { selectionArea: any; } -CommandsRegistry.registerCommand('_webview.openDevTools', function () { - const elements = document.querySelectorAll('webview.ready'); - for (let i = 0; i < elements.length; i++) { - try { - (elements.item(i)).openDevTools(); - } catch (e) { - console.error(e); - } - } -}); - -MenuRegistry.addCommand({ - id: '_webview.openDevTools', - title: localize('devtools.webview', "Developer: Webview Tools") -}); - type ApiThemeClassName = 'vscode-light' | 'vscode-dark' | 'vscode-high-contrast'; export interface WebviewOptions { @@ -83,7 +62,6 @@ export default class Webview { private _ready: TPromise; private _disposables: IDisposable[] = []; private _onDidClickLink = new Emitter(); - private _onDidLoadContent = new Emitter<{ stats: any }>(); private _onDidScroll = new Emitter<{ scrollYPercentage: number }>(); private _onFoundInPageResults = new Emitter(); @@ -99,19 +77,18 @@ export default class Webview { private _options: WebviewOptions = {}, ) { this._webview = document.createElement('webview'); - - this._webview.style.width = '100%'; - this._webview.style.height = '100%'; - this._webview.style.outline = '0'; - this._webview.style.opacity = '0'; - this._webview.contextIsolation = true; + this._webview.setAttribute('partition', this._options.allowSvgs ? 'webview' : `webview${Webview.index++}`); // disable auxclick events (see https://developers.google.com/web/updates/2016/10/auxclick) this._webview.setAttribute('disableblinkfeatures', 'Auxclick'); this._webview.setAttribute('disableguestresize', ''); this._webview.setAttribute('webpreferences', 'contextIsolation=yes'); - this._webview.setAttribute('partition', `webview${Webview.index++}`); + + this._webview.style.width = '100%'; + this._webview.style.height = '100%'; + this._webview.style.outline = '0'; + this._webview.style.opacity = '0'; this._webview.preload = require.toUrl('./webview-pre.js'); this._webview.src = require.toUrl('./webview.html'); @@ -185,8 +162,6 @@ export default class Webview { if (event.channel === 'did-set-content') { this._webview.style.opacity = ''; - let [stats] = event.args; - this._onDidLoadContent.fire({ stats }); this.layout(); return; } @@ -229,7 +204,6 @@ export default class Webview { dispose(): void { this._onDidClickLink.dispose(); - this._onDidLoadContent.dispose(); this._disposables = dispose(this._disposables); if (this._webview.parentElement) { @@ -243,10 +217,6 @@ export default class Webview { return this._onDidClickLink.event; } - get onDidLoadContent(): Event<{ stats: any }> { - return this._onDidLoadContent.event; - } - get onDidScroll(): Event<{ scrollYPercentage: number }> { return this._onDidScroll.event; } diff --git a/src/vs/workbench/parts/html/browser/webviewEditor.ts b/src/vs/workbench/parts/html/browser/webviewEditor.ts index f7df65f8d8d..decb70ff679 100644 --- a/src/vs/workbench/parts/html/browser/webviewEditor.ts +++ b/src/vs/workbench/parts/html/browser/webviewEditor.ts @@ -22,12 +22,6 @@ export interface HtmlPreviewEditorViewState { scrollYPercentage: number; } -interface HtmlPreviewEditorViewStates { - 0?: HtmlPreviewEditorViewState; - 1?: HtmlPreviewEditorViewState; - 2?: HtmlPreviewEditorViewState; -} - /** A context key that is set when a webview editor has focus. */ export const KEYBINDING_CONTEXT_WEBVIEWEDITOR_FOCUS = new RawContextKey('webviewEditorFocus', undefined); /** A context key that is set when a webview editor does not have focus. */ diff --git a/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts b/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts index b63be809d92..1af4bf2c994 100644 --- a/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts +++ b/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts @@ -635,6 +635,8 @@ class DefaultSettingsEditorContribution extends Disposable implements ISettingsE const model = this.editor.getModel(); if (model) { this.preferencesRenderer = this._createPreferencesRenderer(); + } else { + this.disposePreferencesRenderer(); } } @@ -671,7 +673,7 @@ class DefaultSettingsEditorContribution extends Disposable implements ISettingsE }); } - dispose() { + private disposePreferencesRenderer(): void { if (this.preferencesRenderer) { this.preferencesRenderer.then(preferencesRenderer => { if (preferencesRenderer) { @@ -682,6 +684,10 @@ class DefaultSettingsEditorContribution extends Disposable implements ISettingsE } }); } + } + + dispose() { + this.disposePreferencesRenderer(); super.dispose(); } } diff --git a/src/vs/workbench/parts/preferences/browser/preferencesService.ts b/src/vs/workbench/parts/preferences/browser/preferencesService.ts index d3355b34349..9d4a5661fdb 100644 --- a/src/vs/workbench/parts/preferences/browser/preferencesService.ts +++ b/src/vs/workbench/parts/preferences/browser/preferencesService.ts @@ -260,8 +260,9 @@ export class PreferencesService extends Disposable implements IPreferencesServic case ConfigurationTarget.USER: return URI.file(this.environmentService.appSettingsPath); case ConfigurationTarget.WORKSPACE: - if (this.contextService.hasWorkspace()) { - return this.contextService.toResource('.vscode/settings.json'); // TODO@Sandeep (https://github.com/Microsoft/vscode/issues/29456) + const workspace = this.contextService.getWorkspace(); + if (workspace) { + return workspace.configuration || this.contextService.toResource('.vscode/settings.json'); } } return null; diff --git a/src/vs/workbench/parts/relauncher/electron-browser/relauncher.contribution.ts b/src/vs/workbench/parts/relauncher/electron-browser/relauncher.contribution.ts index 9941724b56c..7e75e373e2f 100644 --- a/src/vs/workbench/parts/relauncher/electron-browser/relauncher.contribution.ts +++ b/src/vs/workbench/parts/relauncher/electron-browser/relauncher.contribution.ts @@ -10,13 +10,13 @@ import { IWorkbenchContributionsRegistry, IWorkbenchContribution, Extensions as import { Registry } from 'vs/platform/registry/common/platform'; import { IMessageService } from 'vs/platform/message/common/message'; import { IPreferencesService } from 'vs/workbench/parts/preferences/common/preferences'; -import { IWindowsService, IWindowService, IWindowConfiguration } from 'vs/platform/windows/common/windows'; +import { IWindowsService, IWindowService, IWindowsConfiguration } from 'vs/platform/windows/common/windows'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { localize } from 'vs/nls'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -interface IConfiguration extends IWindowConfiguration { +interface IConfiguration extends IWindowsConfiguration { update: { channel: string; }; telemetry: { enableCrashReporter: boolean }; } @@ -30,6 +30,7 @@ export class SettingsChangeRelauncher implements IWorkbenchContribution { private updateChannel: string; private enableCrashReporter: boolean; private rootCount: number; + private firstRootPath: string; constructor( @IWindowsService private windowsService: IWindowsService, @@ -41,6 +42,7 @@ export class SettingsChangeRelauncher implements IWorkbenchContribution { @IWorkspaceContextService private contextService: IWorkspaceContextService ) { this.rootCount = this.contextService.hasWorkspace() ? this.contextService.getWorkspace().roots.length : 0; + this.firstRootPath = this.contextService.hasWorkspace() ? this.contextService.getWorkspace().roots[0].fsPath : void 0; this.onConfigurationChange(configurationService.getConfiguration(), false); this.registerListeners(); @@ -91,6 +93,7 @@ export class SettingsChangeRelauncher implements IWorkbenchContribution { private onDidChangeWorkspaceRoots(): void { const newRootCount = this.contextService.hasWorkspace() ? this.contextService.getWorkspace().roots.length : 0; + const newFirstRootPath = this.contextService.hasWorkspace() ? this.contextService.getWorkspace().roots[0].fsPath : void 0; let reload = false; if (this.rootCount === 0 && newRootCount > 0) { @@ -99,7 +102,12 @@ export class SettingsChangeRelauncher implements IWorkbenchContribution { reload = true; // transition: from 1+ folders to 0 } + if (this.firstRootPath !== newFirstRootPath) { + reload = true; // first root folder changed + } + this.rootCount = newRootCount; + this.firstRootPath = newFirstRootPath; if (reload) { this.doConfirm( diff --git a/src/vs/workbench/parts/scm/electron-browser/deploymentView.ts b/src/vs/workbench/parts/scm/electron-browser/deploymentView.ts new file mode 100644 index 00000000000..0b9e57f1d31 --- /dev/null +++ b/src/vs/workbench/parts/scm/electron-browser/deploymentView.ts @@ -0,0 +1,160 @@ +/*--------------------------------------------------------------------------------------------- + * 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 'vs/css!./media/deploymentView'; +import nls = require('vs/nls'); +import * as errors from 'vs/base/common/errors'; +import DOM = require('vs/base/browser/dom'); +import { TPromise } from 'vs/base/common/winjs.base'; +import { IAction } from 'vs/base/common/actions'; +import { $ } from 'vs/base/browser/builder'; +import { IActionItem } from 'vs/base/browser/ui/actionbar/actionbar'; +import { CollapsibleView, IViewletViewOptions, IViewOptions } from 'vs/workbench/parts/views/browser/views'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { ViewSizing } from 'vs/base/browser/ui/splitview/splitview'; +import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { focusBorder, textLinkForeground, textLinkActiveForeground } from 'vs/platform/theme/common/colorRegistry'; +import { IQuickOpenService } from 'vs/platform/quickOpen/common/quickOpen'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import URI from 'vs/base/common/uri'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; + + +const TARGETS = [ + { + id: 'azure', + label: nls.localize('deployToAzure', "Deploy to Azure App Service"), + uri: () => URI.parse('https://code.visualstudio.com/nodejs-deployment#vscode') + }, + { + id: 'aws', + label: nls.localize('deployToAWS', "Deploy to Elastic Beanstalk (AWS)"), + uri: () => URI.parse('http://docs.aws.amazon.com/elasticbeanstalk/latest/dg/eb3-cli-git.html') + }, + { + id: 'heroku', + label: nls.localize('deployToHeroku', "Deploy to Heroku"), + uri: () => URI.parse('https://devcenter.heroku.com/articles/git') + }, +]; + +export class DeploymentView extends CollapsibleView { + + public static ID: string = 'workbench.deploymentView'; + public static NAME = nls.localize('deployment', "Deployment"); + public static HEADER_HEIGHT = 22; + public static HEIGHT = 80; + + private deploymentLink: HTMLAnchorElement; + + constructor( + options: IViewletViewOptions, + @IThemeService private themeService: IThemeService, + @IInstantiationService private instantiationService: IInstantiationService, + @IQuickOpenService private quickOpenService: IQuickOpenService, + @IOpenerService private openerService: IOpenerService, + @ITelemetryService private telemetryService: ITelemetryService, + @IKeybindingService keybindingService: IKeybindingService, + @IContextMenuService contextMenuService: IContextMenuService + ) { + super({ ...(options as IViewOptions), ariaHeaderLabel: nls.localize('deploymentSection', "Deployment Section"), sizing: ViewSizing.Fixed }, keybindingService, contextMenuService); + } + + public renderHeader(container: HTMLElement): void { + let titleDiv = $('div.title').appendTo(container); + $('span').text(this.name).appendTo(titleDiv); + } + + protected renderBody(container: HTMLElement): void { + DOM.addClass(container, 'deployment-view'); + + let section = $('div.section').appendTo(container); + + this.deploymentLink = document.createElement('a'); + this.deploymentLink.innerText = nls.localize('deployApplication', "Deploy your application using Git"); + this.deploymentLink.classList.add('pointer'); + this.deploymentLink.classList.add('prominent'); + this.deploymentLink.tabIndex = 0; + this.deploymentLink.href = 'javascript:void(0)'; + this.deploymentLink.addEventListener('click', e => { + this.pickTargetService(); + e.preventDefault(); + e.stopPropagation(); + }); + section.append(this.deploymentLink); + } + + private pickTargetService() { + this.telemetryService.publicLog('deploymentClicked'); + this.quickOpenService.pick(TARGETS, { placeHolder: nls.localize('pickTargetService', "Pick target service") }) + .then(pick => { + if (pick) { + this.telemetryService.publicLog('deploymentServicePicked', { id: pick.id }); + } else { + this.telemetryService.publicLog('deploymentCanceled'); + } + return pick && this.openerService.open(pick.uri()); + }) + .then(null, errors.onUnexpectedError); + } + + layoutBody(size: number): void { + // no-op + } + + public create(): TPromise { + return TPromise.as(null); + } + + public setVisible(visible: boolean): TPromise { + return TPromise.as(null); + } + + public focusBody(): void { + if (this.deploymentLink) { + this.deploymentLink.focus(); + } + } + + protected reveal(element: any, relativeTop?: number): TPromise { + return TPromise.as(null); + } + + public getActions(): IAction[] { + return []; + } + + public getSecondaryActions(): IAction[] { + return []; + } + + public getActionItem(action: IAction): IActionItem { + return null; + } + + public shutdown(): void { + } +} + +// theming + +registerThemingParticipant((theme, collector) => { + const link = theme.getColor(textLinkForeground); + if (link) { + collector.addRule(`.deployment-view a { color: ${link}; }`); + } + const activeLink = theme.getColor(textLinkActiveForeground); + if (activeLink) { + collector.addRule(`.deployment-view a:hover, + .deployment-view a:active { color: ${activeLink}; }`); + } + const focusColor = theme.getColor(focusBorder); + if (focusColor) { + collector.addRule(`.deployment-view a:focus { outline-color: ${focusColor}; }`); + } +}); diff --git a/src/vs/workbench/parts/scm/electron-browser/media/deploymentView.css b/src/vs/workbench/parts/scm/electron-browser/media/deploymentView.css new file mode 100644 index 00000000000..1f4ab95c2ac --- /dev/null +++ b/src/vs/workbench/parts/scm/electron-browser/media/deploymentView.css @@ -0,0 +1,17 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.deployment-view { + padding: 7px 19px; +} + +.deployment-view a { + text-decoration: none; +} + +.deployment-view a:focus { + outline: 1px solid -webkit-focus-ring-color; + outline-offset: -1px; +} diff --git a/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts b/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts index 1d4064b4fb3..92f3373f859 100644 --- a/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts +++ b/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts @@ -46,6 +46,9 @@ import { comparePaths } from 'vs/base/common/comparers'; import { isSCMResource } from './scmUtil'; import { attachInputBoxStyler, attachListStyler, attachBadgeStyler } from 'vs/platform/theme/common/styler'; import Severity from 'vs/base/common/severity'; +import { IViewletViewOptions } from 'vs/workbench/parts/views/browser/views'; +import { SplitView } from 'vs/base/browser/ui/splitview/splitview'; +import { DeploymentView } from './deploymentView'; // TODO@Joao // Need to subclass MenuItemActionItem in order to respect @@ -228,6 +231,7 @@ export class SCMViewlet extends Viewlet { private inputBox: InputBox; private listContainer: HTMLElement; private list: List; + private deploymentSplitView: SplitView; private menus: SCMMenus; private providerChangeDisposable: IDisposable = EmptyDisposable; private disposables: IDisposable[] = []; @@ -258,7 +262,11 @@ export class SCMViewlet extends Viewlet { private setActiveProvider(activeProvider: ISCMProvider | undefined): void { this.providerChangeDisposable.dispose(); + const updateLayout = (this.activeProvider && this.activeProvider.id === 'git') !== (activeProvider && activeProvider.id === 'git'); this.activeProvider = activeProvider; + if (updateLayout) { + this.layout(); + } if (activeProvider) { const disposables = [activeProvider.onDidChange(this.update, this)]; @@ -316,6 +324,19 @@ export class SCMViewlet extends Viewlet { keyboardSupport: false }); + const options: IViewletViewOptions = { + id: DeploymentView.ID, + name: DeploymentView.NAME, + actionRunner: this.actionRunner, + collapsed: false, + viewletSettings: null + }; + this.deploymentSplitView = new SplitView(root); + const view = this.instantiationService.createInstance(DeploymentView, options); + this.disposables.push(view.addListener('change', () => this.layout())); + this.deploymentSplitView.addView(view); + this.disposables.push(this.deploymentSplitView); + this.disposables.push(attachListStyler(this.list, this.themeService)); this.disposables.push(this.listService.register(this.list)); @@ -390,10 +411,13 @@ export class SCMViewlet extends Viewlet { this.inputBox.layout(); const editorHeight = this.inputBox.height; - const listHeight = dimension.height - (editorHeight + 12 /* margin */); + const deploymentHeight = this.activeProvider && this.activeProvider.id === 'git' ? (this.deploymentSplitView.getViews()[0].isExpanded() ? DeploymentView.HEIGHT : DeploymentView.HEADER_HEIGHT) : 0; + const listHeight = dimension.height - (editorHeight + 12 /* margin */) - deploymentHeight; this.listContainer.style.height = `${listHeight}px`; this.list.layout(listHeight); + this.deploymentSplitView.layout(deploymentHeight); + toggleClass(this.inputBoxContainer, 'scroll', editorHeight >= 134); } diff --git a/src/vs/workbench/parts/search/browser/searchViewlet.ts b/src/vs/workbench/parts/search/browser/searchViewlet.ts index 64568249dec..5fff6e05f3c 100644 --- a/src/vs/workbench/parts/search/browser/searchViewlet.ts +++ b/src/vs/workbench/parts/search/browser/searchViewlet.ts @@ -51,7 +51,7 @@ import { RefreshAction, CollapseAllAction, ClearSearchResultsAction, ConfigureGl import { IReplaceService } from 'vs/workbench/parts/search/common/replace'; import Severity from 'vs/base/common/severity'; import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; -import { OpenFolderAction, OpenFileFolderAction } from 'vs/workbench/browser/actions/fileActions'; +import { OpenFolderAction, OpenFileFolderAction } from 'vs/workbench/browser/actions/workspaceActions'; import * as Constants from 'vs/workbench/parts/search/common/constants'; import { IListService } from 'vs/platform/list/browser/listService'; import { IThemeService, ITheme, ICssStyleCollector, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; diff --git a/src/vs/workbench/parts/tasks/electron-browser/media/task.contribution.css b/src/vs/workbench/parts/tasks/electron-browser/media/task.contribution.css index c02899f2f02..962f7118fee 100644 --- a/src/vs/workbench/parts/tasks/electron-browser/media/task.contribution.css +++ b/src/vs/workbench/parts/tasks/electron-browser/media/task.contribution.css @@ -13,6 +13,12 @@ padding: 0 5px 0 5px; } +.task-statusbar-runningItem-label > .octicon { + font-size: 11px; + vertical-align: -webkit-baseline-middle; + height: 19px; +} + .task-statusbar-item { display: inline-block; } diff --git a/src/vs/workbench/parts/tasks/electron-browser/task.contribution.ts b/src/vs/workbench/parts/tasks/electron-browser/task.contribution.ts index c1850846978..bf8da6b58a5 100644 --- a/src/vs/workbench/parts/tasks/electron-browser/task.contribution.ts +++ b/src/vs/workbench/parts/tasks/electron-browser/task.contribution.ts @@ -26,6 +26,7 @@ import * as strings from 'vs/base/common/strings'; import { ValidationStatus, ValidationState } from 'vs/base/common/parsers'; import * as UUID from 'vs/base/common/uuid'; import { LinkedMap, Touch } from 'vs/base/common/map'; +import { OcticonLabel } from 'vs/base/browser/ui/octiconLabel/octiconLabel'; import { Registry } from 'vs/platform/registry/common/platform'; import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; @@ -473,29 +474,31 @@ class TaskStatusBarItem extends Themable implements IStatusbarItem { } public render(container: HTMLElement): IDisposable { + let callOnDispose: IDisposable[] = []; - - const element = document.createElement('div'); - const label = document.createElement('a'); - + const element = document.createElement('a'); Dom.addClass(element, 'task-statusbar-runningItem'); - Dom.addClass(label, 'task-statusbar-runningItem-label'); - element.appendChild(label); - element.title = nls.localize('runningTasks', "Show Running Tasks"); + let labelElement = document.createElement('div'); + Dom.addClass(labelElement, 'task-statusbar-runningItem-label'); + element.appendChild(labelElement); - callOnDispose.push(Dom.addDisposableListener(label, 'click', (e: MouseEvent) => { + let label = new OcticonLabel(labelElement); + label.title = nls.localize('runningTasks', "Show Running Tasks"); + + $(element).hide(); + + callOnDispose.push(Dom.addDisposableListener(labelElement, 'click', (e: MouseEvent) => { (this.taskService as TaskService).runShowTasks(); })); let updateStatus = (): void => { this.taskService.getActiveTasks().then(tasks => { if (tasks.length === 0) { - label.innerHTML = nls.localize('nothingRunner', 'Running Tasks: 0'); - } else if (tasks.length === 1) { - label.innerHTML = nls.localize('oneTasksRunnering', 'Running Tasks: 1'); + $(element).hide(); } else { - label.innerHTML = nls.localize('nTasksRunnering', 'Running Tasks: {0}', tasks.length); + label.text = `$(tools) ${tasks.length}`; + $(element).show(); } }); }; @@ -944,6 +947,9 @@ class TaskService extends EventEmitter implements ITaskService { let entries: ProblemMatcherPickEntry[] = []; for (let key of ProblemMatcherRegistry.keys()) { let matcher = ProblemMatcherRegistry.get(key); + if (matcher.deprecated) { + continue; + } if (matcher.name === matcher.label) { entries.push({ label: matcher.name, matcher: matcher }); } else { @@ -1027,6 +1033,9 @@ class TaskService extends EventEmitter implements ITaskService { let identifier: TaskConfig.TaskIdentifier = Objects.assign(Object.create(null), task.defines); delete identifier['_key']; Object.keys(identifier).forEach(key => toCustomize[key] = identifier[key]); + if (task.problemMatchers && task.problemMatchers.length > 0 && Types.isStringArray(task.problemMatchers)) { + toCustomize.problemMatcher = task.problemMatchers; + } } if (!toCustomize) { return TPromise.as(undefined); @@ -1039,7 +1048,7 @@ class TaskService extends EventEmitter implements ITaskService { } } } else { - if (task.problemMatchers === void 0 || task.problemMatchers.length === 0) { + if (toCustomize.problemMatcher === void 0 && task.problemMatchers === void 0 || task.problemMatchers.length === 0) { toCustomize.problemMatcher = []; } } diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminalColorRegistry.ts b/src/vs/workbench/parts/terminal/electron-browser/terminalColorRegistry.ts index 4386fa8716b..a9b56906ec4 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/terminalColorRegistry.ts +++ b/src/vs/workbench/parts/terminal/electron-browser/terminalColorRegistry.ts @@ -19,6 +19,9 @@ export const TERMINAL_FOREGROUND_COLOR = registerColor('terminal.foreground', { dark: '#CCCCCC', hc: '#FFFFFF' }, nls.localize('terminal.foreground', 'The foreground color of the terminal.')); +export const TERMINAL_CURSOR_FOREGROUND_COLOR = registerColor('terminalCursor.foreground', null, nls.localize('terminalCursor.foreground', 'The foreground color of the terminal cursor.')); +export const TERMINAL_CURSOR_BACKGROUND_COLOR = registerColor('terminalCursor.background', null, nls.localize('terminalCursor.background', 'The background color of the terminal cursor. Allows customizing the color of a character overlapped by a block cursor.')); + // TODO: Reinstate, see #28397 // export const TERMINAL_SELECTION_BACKGROUND_COLOR = registerColor('terminal.selectionBackground', { // light: '#000', diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminalPanel.ts b/src/vs/workbench/parts/terminal/electron-browser/terminalPanel.ts index 1b550324c4c..843d71e1ffc 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/terminalPanel.ts +++ b/src/vs/workbench/parts/terminal/electron-browser/terminalPanel.ts @@ -16,8 +16,9 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { ITerminalService, ITerminalFont, TERMINAL_PANEL_ID } from 'vs/workbench/parts/terminal/common/terminal'; import { IThemeService, ITheme } from 'vs/platform/theme/common/themeService'; import { TerminalFindWidget } from './terminalFindWidget'; -import { ansiColorIdentifiers, TERMINAL_BACKGROUND_COLOR, TERMINAL_FOREGROUND_COLOR } from './terminalColorRegistry'; +import { ansiColorIdentifiers, TERMINAL_BACKGROUND_COLOR, TERMINAL_FOREGROUND_COLOR, TERMINAL_CURSOR_FOREGROUND_COLOR, TERMINAL_CURSOR_BACKGROUND_COLOR } from './terminalColorRegistry'; import { ColorIdentifier, editorHoverBackground, editorHoverBorder, editorForeground } from 'vs/platform/theme/common/colorRegistry'; +import { PANEL_BACKGROUND } from 'vs/workbench/common/theme'; import { KillTerminalAction, CreateNewTerminalAction, SwitchTerminalInstanceAction, SwitchTerminalInstanceActionItem, CopyTerminalSelectionAction, TerminalPasteAction, ClearTerminalAction, SelectAllTerminalAction } from 'vs/workbench/parts/terminal/electron-browser/terminalActions'; import { Panel } from 'vs/workbench/browser/panel'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; @@ -275,15 +276,26 @@ export class TerminalPanel extends Panel { } const fgColor = theme.getColor(TERMINAL_FOREGROUND_COLOR); if (fgColor) { - css += `.monaco-workbench .panel.integrated-terminal .xterm { color: ${fgColor}; }` + - `.monaco-workbench .panel.integrated-terminal .xterm:not(.xterm-cursor-style-underline):not(.xterm-cursor-style-bar).focus .terminal-cursor,` + - `.monaco-workbench .panel.integrated-terminal .xterm:not(.xterm-cursor-style-underline):not(.xterm-cursor-style-bar):focus .terminal-cursor { background-color: ${fgColor} }` + - `.monaco-workbench .panel.integrated-terminal .xterm:not(.focus):not(:focus) .terminal-cursor { outline-color: ${fgColor}; }` + - `.monaco-workbench .panel.integrated-terminal .xterm.xterm-cursor-style-bar .terminal-cursor::before,` + - `.monaco-workbench .panel.integrated-terminal .xterm.xterm-cursor-style-underline .terminal-cursor::before { background-color: ${fgColor}; }` + - `.monaco-workbench .panel.integrated-terminal .xterm.xterm-cursor-style-bar.focus.xterm-cursor-blink .terminal-cursor::before,` + - `.monaco-workbench .panel.integrated-terminal .xterm.xterm-cursor-style-underline.focus.xterm-cursor-blink .terminal-cursor::before { background-color: ${fgColor}; }`; + css += `.monaco-workbench .panel.integrated-terminal .xterm { color: ${fgColor}; }`; } + + const cursorFgColor = theme.getColor(TERMINAL_CURSOR_FOREGROUND_COLOR) || fgColor; + if (cursorFgColor) { + css += `.monaco-workbench .panel.integrated-terminal .xterm:not(.xterm-cursor-style-underline):not(.xterm-cursor-style-bar).focus .terminal-cursor,` + + `.monaco-workbench .panel.integrated-terminal .xterm:not(.xterm-cursor-style-underline):not(.xterm-cursor-style-bar):focus .terminal-cursor { background-color: ${cursorFgColor} }` + + `.monaco-workbench .panel.integrated-terminal .xterm:not(.focus):not(:focus) .terminal-cursor { outline-color: ${cursorFgColor}; }` + + `.monaco-workbench .panel.integrated-terminal .xterm.xterm-cursor-style-bar .terminal-cursor::before,` + + `.monaco-workbench .panel.integrated-terminal .xterm.xterm-cursor-style-underline .terminal-cursor::before { background-color: ${cursorFgColor}; }` + + `.monaco-workbench .panel.integrated-terminal .xterm.xterm-cursor-style-bar.focus.xterm-cursor-blink .terminal-cursor::before,` + + `.monaco-workbench .panel.integrated-terminal .xterm.xterm-cursor-style-underline.focus.xterm-cursor-blink .terminal-cursor::before { background-color: ${cursorFgColor}; }`; + } + + const cursorBgColor = theme.getColor(TERMINAL_CURSOR_BACKGROUND_COLOR) || bgColor || theme.getColor(PANEL_BACKGROUND); + if (cursorBgColor) { + css += `.monaco-workbench .panel.integrated-terminal .xterm:not(.xterm-cursor-style-underline):not(.xterm-cursor-style-bar).focus .terminal-cursor,` + + `.monaco-workbench .panel.integrated-terminal .xterm:not(.xterm-cursor-style-underline):not(.xterm-cursor-style-bar):focus .terminal-cursor { color: ${cursorBgColor} }`; + } + // TODO: Reinstate, see #28397 // const selectionColor = theme.getColor(TERMINAL_SELECTION_BACKGROUND_COLOR); // if (selectionColor) { diff --git a/src/vs/workbench/parts/views/browser/views.ts b/src/vs/workbench/parts/views/browser/views.ts index 07b381674c6..2986eb9b7a4 100644 --- a/src/vs/workbench/parts/views/browser/views.ts +++ b/src/vs/workbench/parts/views/browser/views.ts @@ -48,7 +48,7 @@ export interface IViewOptions { export interface IViewConstructorSignature { - new (options: IViewOptions, ...services: { _serviceBrand: any; }[]): IView; + new(options: IViewOptions, ...services: { _serviceBrand: any; }[]): IView; } @@ -638,7 +638,7 @@ export class ComposedViewsViewlet extends Viewlet { getAnchor: () => anchor, getActions: () => TPromise.as([{ id: `${view.id}.removeView`, - label: nls.localize('removeView', "Remove from {0}", this.getTitle()), + label: nls.localize('removeView', "Remove from Side Bar"), enabled: true, run: () => this.toggleViewVisibility(view.id) }]), diff --git a/src/vs/workbench/parts/views/browser/viewsRegistry.ts b/src/vs/workbench/parts/views/browser/viewsRegistry.ts index c10d1922f82..e6badc6ed3e 100644 --- a/src/vs/workbench/parts/views/browser/viewsRegistry.ts +++ b/src/vs/workbench/parts/views/browser/viewsRegistry.ts @@ -24,6 +24,7 @@ export class ViewLocation { static getContributedViewLocation(value: string): ViewLocation { switch (value) { case ViewLocation.Explorer.id: return ViewLocation.Explorer; + case ViewLocation.Debug.id: return ViewLocation.Debug; } return void 0; } diff --git a/src/vs/workbench/parts/watermark/electron-browser/watermark.ts b/src/vs/workbench/parts/watermark/electron-browser/watermark.ts index a297c50d0b7..38a357f9de9 100644 --- a/src/vs/workbench/parts/watermark/electron-browser/watermark.ts +++ b/src/vs/workbench/parts/watermark/electron-browser/watermark.ts @@ -20,7 +20,7 @@ import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { OpenRecentAction } from 'vs/workbench/electron-browser/actions'; import { GlobalNewUntitledFileAction, OpenFileAction } from 'vs/workbench/parts/files/browser/fileActions'; -import { OpenFolderAction, OpenFileFolderAction } from 'vs/workbench/browser/actions/fileActions'; +import { OpenFolderAction, OpenFileFolderAction } from 'vs/workbench/browser/actions/workspaceActions'; import { ShowAllCommandsAction } from 'vs/workbench/parts/quickopen/browser/commandsHandler'; import { Parts, IPartService } from 'vs/workbench/services/part/common/partService'; import { StartAction } from 'vs/workbench/parts/debug/browser/debugActions'; diff --git a/src/vs/workbench/parts/welcome/page/electron-browser/welcomePage.contribution.ts b/src/vs/workbench/parts/welcome/page/electron-browser/welcomePage.contribution.ts index b924959f037..4320700e56e 100644 --- a/src/vs/workbench/parts/welcome/page/electron-browser/welcomePage.contribution.ts +++ b/src/vs/workbench/parts/welcome/page/electron-browser/welcomePage.contribution.ts @@ -27,7 +27,7 @@ Registry.as(ConfigurationExtensions.Configuration) localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'workbench.startupEditor.newUntitledFile' }, "Open a new untitled file."), ], 'default': 'welcomePage', - 'description': localize('workbench.startupEditor', "Controls which editor is shown at startup, if none is restored from the previous session. Select 'none' to start without an editor, 'welcomePage' to open the Welcome page (default), 'newUntitledFile' to open a new untitled file (only when not opening a folder).") + 'description': localize('workbench.startupEditor', "Controls which editor is shown at startup, if none is restored from the previous session. Select 'none' to start without an editor, 'welcomePage' to open the Welcome page (default), 'newUntitledFile' to open a new untitled file (only opening an empty workspace).") }, } }); diff --git a/src/vs/workbench/parts/welcome/page/electron-browser/welcomePage.ts b/src/vs/workbench/parts/welcome/page/electron-browser/welcomePage.ts index d8f457f5577..821860aca7f 100644 --- a/src/vs/workbench/parts/welcome/page/electron-browser/welcomePage.ts +++ b/src/vs/workbench/parts/welcome/page/electron-browser/welcomePage.ts @@ -39,6 +39,7 @@ import { registerColor, focusBorder, textLinkForeground, textLinkActiveForegroun import { getExtraColor } from 'vs/workbench/parts/welcome/walkThrough/node/walkThroughUtils'; import { IExtensionsWorkbenchService } from 'vs/workbench/parts/extensions/common/extensions'; import { IStorageService } from 'vs/platform/storage/common/storage'; +import { IWorkspaceIdentifier, getWorkspaceLabel, ISingleFolderWorkspaceIdentifier } from "vs/platform/workspaces/common/workspaces"; used(); @@ -188,7 +189,7 @@ class WelcomePage { } private create() { - const recentlyOpened = this.windowService.getRecentlyOpen(); + const recentlyOpened = this.windowService.getRecentlyOpened(); const installedExtensions = this.instantiationService.invokeFunction(getInstalledExtensions); const uri = URI.parse(require.toUrl('./vs_code_welcome_page')) .with({ @@ -200,7 +201,7 @@ class WelcomePage { .then(null, onUnexpectedError); } - private onReady(container: HTMLElement, recentlyOpened: TPromise<{ files: string[]; folders: string[]; }>, installedExtensions: TPromise): void { + private onReady(container: HTMLElement, recentlyOpened: TPromise<{ files: string[]; workspaces: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier)[]; }>, installedExtensions: TPromise): void { const enabled = isWelcomePageEnabled(this.configurationService); const showOnStartup = container.querySelector('#showOnStartup'); if (enabled) { @@ -210,24 +211,36 @@ class WelcomePage { this.configurationEditingService.writeConfiguration(ConfigurationTarget.USER, { key: configurationKey, value: showOnStartup.checked ? 'welcomePage' : 'newUntitledFile' }); }); - recentlyOpened.then(({ folders }) => { - if (this.contextService.hasWorkspace()) { - const currents = this.contextService.getWorkspace().roots; - folders = folders.filter(folder => !currents.some(current => this.pathEquals(folder, current.fsPath))); - } - if (!folders.length) { + recentlyOpened.then(({ workspaces }) => { + const context = this.contextService.getWorkspace(); + workspaces = workspaces.filter(workspace => { + if (this.contextService.hasMultiFolderWorkspace() && typeof workspace !== 'string' && context.id === workspace.id) { + return false; // do not show current workspace + } + + if (this.contextService.hasFolderWorkspace() && typeof workspace === 'string' && this.pathEquals(context.roots[0].fsPath, workspace)) { + return false; // do not show current workspace (single folder case) + } + + return true; + }); + if (!workspaces.length) { const recent = container.querySelector('.welcomePage') as HTMLElement; recent.classList.add('emptyRecent'); return; } const ul = container.querySelector('.recent ul'); const before = ul.firstElementChild; - folders.slice(0, 5).forEach(folder => { + workspaces.slice(0, 5).forEach(workspace => { + const label = (typeof workspace === 'string') ? path.basename(workspace) : getWorkspaceLabel(this.environmentService, workspace); + const parent = (typeof workspace === 'string') ? path.dirname(workspace) : ''; + const wsPath = (typeof workspace === 'string') ? workspace : workspace.configPath; + const li = document.createElement('li'); const a = document.createElement('a'); - let name = path.basename(folder); - let parentFolder = path.dirname(folder); + let name = label; + let parentFolder = parent; if (!name && parentFolder) { const tmp = name; name = parentFolder; @@ -236,7 +249,7 @@ class WelcomePage { const tildifiedParentFolder = tildify(parentFolder, this.environmentService.userHome); a.innerText = name; - a.title = folder; + a.title = label; a.setAttribute('aria-label', localize('welcomePage.openFolderWithPath', "Open folder {0} with path {1}", name, tildifiedParentFolder)); a.href = 'javascript:void(0)'; a.addEventListener('click', e => { @@ -244,7 +257,7 @@ class WelcomePage { id: 'openRecentFolder', from: telemetryFrom }); - this.windowsService.openWindow([folder], { forceNewWindow: e.ctrlKey || e.metaKey }); + this.windowsService.openWindow([wsPath], { forceNewWindow: e.ctrlKey || e.metaKey }); e.preventDefault(); e.stopPropagation(); }); @@ -254,7 +267,7 @@ class WelcomePage { span.classList.add('path'); span.classList.add('detail'); span.innerText = tildifiedParentFolder; - span.title = folder; + span.title = label; li.appendChild(span); ul.insertBefore(li, before); diff --git a/src/vs/workbench/services/configuration/common/configurationModels.ts b/src/vs/workbench/services/configuration/common/configurationModels.ts index 66cf0b79077..ab1d0a40140 100644 --- a/src/vs/workbench/services/configuration/common/configurationModels.ts +++ b/src/vs/workbench/services/configuration/common/configurationModels.ts @@ -5,12 +5,77 @@ 'use strict'; import { clone } from 'vs/base/common/objects'; +import URI from 'vs/base/common/uri'; +import { Schemas } from 'vs/base/common/network'; +import { distinct } from 'vs/base/common/arrays'; import { CustomConfigurationModel, toValuesTree } from 'vs/platform/configuration/common/model'; import { ConfigurationModel } from 'vs/platform/configuration/common/configuration'; import { Registry } from 'vs/platform/registry/common/platform'; import { IConfigurationRegistry, IConfigurationPropertySchema, Extensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; import { WORKSPACE_STANDALONE_CONFIGURATIONS } from 'vs/workbench/services/configuration/common/configuration'; +export class WorkspaceConfigurationModel extends CustomConfigurationModel { + + private _raw: T; + private _folders: URI[]; + private _worksapaceSettings: ConfigurationModel; + private _tasksConfiguration: ConfigurationModel; + private _launchConfiguration: ConfigurationModel; + private _workspaceConfiguration: ConfigurationModel; + + public update(content: string): void { + super.update(content); + this._worksapaceSettings = new ConfigurationModel(this._worksapaceSettings.contents, this._worksapaceSettings.keys, this.overrides); + this._workspaceConfiguration = this.consolidate(); + } + + get id(): string { + return this._raw['id']; + } + + get folders(): URI[] { + return this._folders; + } + + get workspaceConfiguration(): ConfigurationModel { + return this._workspaceConfiguration; + } + + protected processRaw(raw: T): void { + this._raw = raw; + + this._folders = this.parseFolders(); + this._worksapaceSettings = this.parseConfigurationModel('settings'); + this._tasksConfiguration = this.parseConfigurationModel('tasks'); + this._launchConfiguration = this.parseConfigurationModel('launch'); + + super.processRaw(raw); + } + + private parseFolders(): URI[] { + const folders: string[] = this._raw['folders'] || []; + return distinct(folders.map(folder => URI.parse(folder)) + .filter(r => r.scheme === Schemas.file)); // only support files for now ; + } + + private parseConfigurationModel(section: string): ConfigurationModel { + const rawSection = this._raw[section] || {}; + const contents = toValuesTree(rawSection, message => console.error(`Conflict in section '${section}' of workspace configuration file ${message}`)); + return new ConfigurationModel(contents, Object.keys(rawSection)); + } + + private consolidate(): ConfigurationModel { + const keys: string[] = [...this._worksapaceSettings.keys, + ...this._tasksConfiguration.keys.map(key => `tasks.${key}`), + ...this._launchConfiguration.keys.map(key => `launch.${key}`)]; + + return new ConfigurationModel({}, keys) + .merge(this._worksapaceSettings) + .merge(this._tasksConfiguration) + .merge(this._launchConfiguration); + } +} + export class ScopedConfigurationModel extends CustomConfigurationModel { constructor(content: string, name: string, public readonly scope: string) { diff --git a/src/vs/workbench/services/configuration/common/jsonEditing.ts b/src/vs/workbench/services/configuration/common/jsonEditing.ts new file mode 100644 index 00000000000..d7e3ba6d347 --- /dev/null +++ b/src/vs/workbench/services/configuration/common/jsonEditing.ts @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import URI from 'vs/base/common/uri'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { createDecorator, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; + +export const IJSONEditingService = createDecorator('jsonEditingService'); + +export enum JSONEditingErrorCode { + + /** + * Error when trying to write and save to the file while it is dirty in the editor. + */ + ERROR_FILE_DIRTY, + + /** + * Error when trying to write to a file that contains JSON errors. + */ + ERROR_INVALID_FILE +} + +export class JSONEditingError extends Error { + constructor(message: string, public code: JSONEditingErrorCode) { + super(message); + } +} + +export interface IJSONValue { + key: string; + value: any; +} + +export interface IJSONEditingService { + + _serviceBrand: ServiceIdentifier; + + write(resource: URI, value: IJSONValue, save: boolean): TPromise; +} \ No newline at end of file diff --git a/src/vs/workbench/services/configuration/node/configuration.ts b/src/vs/workbench/services/configuration/node/configuration.ts index 1aa619f288c..6219873cad5 100644 --- a/src/vs/workbench/services/configuration/node/configuration.ts +++ b/src/vs/workbench/services/configuration/node/configuration.ts @@ -9,29 +9,31 @@ import * as paths from 'vs/base/common/paths'; import { TPromise } from 'vs/base/common/winjs.base'; import Event, { Emitter } from 'vs/base/common/event'; import { StrictResourceMap } from 'vs/base/common/map'; -import { distinct, equals } from 'vs/base/common/arrays'; +import { equals } from 'vs/base/common/arrays'; import * as objects from 'vs/base/common/objects'; import * as errors from 'vs/base/common/errors'; import * as collections from 'vs/base/common/collections'; -import { Disposable } from 'vs/base/common/lifecycle'; -import { Schemas } from 'vs/base/common/network'; +import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { RunOnceScheduler } from 'vs/base/common/async'; -import { readFile } from 'vs/base/node/pfs'; +import { readFile, stat } from 'vs/base/node/pfs'; import * as extfs from 'vs/base/node/extfs'; import { IWorkspaceContextService, IWorkspace, Workspace, ILegacyWorkspace, LegacyWorkspace } from 'vs/platform/workspace/common/workspace'; import { FileChangeType, FileChangesEvent } from 'vs/platform/files/common/files'; import { isLinux } from 'vs/base/common/platform'; +import { ConfigWatcher } from 'vs/base/node/config'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { CustomConfigurationModel } from 'vs/platform/configuration/common/model'; -import { ScopedConfigurationModel, FolderConfigurationModel, FolderSettingsModel } from 'vs/workbench/services/configuration/common/configurationModels'; +import { WorkspaceConfigurationModel, ScopedConfigurationModel, FolderConfigurationModel, FolderSettingsModel } from 'vs/workbench/services/configuration/common/configurationModels'; import { IConfigurationServiceEvent, ConfigurationSource, IConfigurationKeys, IConfigurationValue, ConfigurationModel, IConfigurationOverrides, Configuration as BaseConfiguration, IConfigurationValues, IConfigurationData } from 'vs/platform/configuration/common/configuration'; import { IWorkspaceConfigurationService, WORKSPACE_CONFIG_FOLDER_DEFAULT_NAME, WORKSPACE_STANDALONE_CONFIGURATIONS, WORKSPACE_CONFIG_DEFAULT_PATH } from 'vs/workbench/services/configuration/common/configuration'; import { ConfigurationService as GlobalConfigurationService } from 'vs/platform/configuration/node/configurationService'; import { basename } from 'path'; -import nls = require('vs/nls'); +import * as nls from 'vs/nls'; import { Registry } from 'vs/platform/registry/common/platform'; import { ExtensionsRegistry, ExtensionMessageCollector } from 'vs/platform/extensions/common/extensionsRegistry'; import { IConfigurationNode, IConfigurationRegistry, Extensions, editorConfigurationSchemaId, IDefaultConfigurationExtension, validateProperty, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; +import { createHash } from 'crypto'; +import { getWorkspaceLabel } from "vs/platform/workspaces/common/workspaces"; interface IStat { resource: URI; @@ -159,72 +161,23 @@ function validateProperties(configuration: IConfigurationNode, collector: Extens } } -export class WorkspaceConfigurationService extends Disposable implements IWorkspaceContextService, IWorkspaceConfigurationService { +export class WorkspaceService extends Disposable implements IWorkspaceConfigurationService, IWorkspaceContextService { public _serviceBrand: any; - private readonly _onDidChangeWorkspaceRoots: Emitter = this._register(new Emitter()); - public readonly onDidChangeWorkspaceRoots: Event = this._onDidChangeWorkspaceRoots.event; + protected workspace: Workspace = null; + protected legacyWorkspace: LegacyWorkspace = null; + protected _configuration: Configuration; - private readonly _onDidUpdateConfiguration: Emitter = this._register(new Emitter()); + protected readonly _onDidUpdateConfiguration: Emitter = this._register(new Emitter()); public readonly onDidUpdateConfiguration: Event = this._onDidUpdateConfiguration.event; - private baseConfigurationService: GlobalConfigurationService; + protected readonly _onDidChangeWorkspaceRoots: Emitter = this._register(new Emitter()); + public readonly onDidChangeWorkspaceRoots: Event = this._onDidChangeWorkspaceRoots.event; - private cachedFolderConfigs: StrictResourceMap>; - - private readonly legacyWorkspace: LegacyWorkspace; - - private _configuration: Configuration; - - constructor(private environmentService: IEnvironmentService, private readonly workspace?: Workspace, private workspaceSettingsRootFolder: string = WORKSPACE_CONFIG_FOLDER_DEFAULT_NAME) { + constructor() { super(); - - this.legacyWorkspace = this.workspace && this.workspace.roots.length ? new LegacyWorkspace(this.workspace.roots[0]) : null; - - this._register(this.onDidUpdateConfiguration(e => this.resolveAdditionalFolders())); - - this.baseConfigurationService = this._register(new GlobalConfigurationService(environmentService)); - this._register(this.baseConfigurationService.onDidUpdateConfiguration(e => this.onBaseConfigurationChanged(e))); - this._register(this.onDidChangeWorkspaceRoots(e => this.onRootsChanged())); - - this.initCaches(); - } - - private resolveAdditionalFolders(): void { - // TODO@Ben multi root - if (!this.workspace || this.environmentService.appQuality === 'stable') { - return; // no additional folders for empty workspaces or in stable - } - - // Resovled configured folders for workspace - let [master] = this.workspace.roots; - let configuredFolders: URI[] = [master]; - const config = this.getConfiguration('workspace'); - if (config) { - const workspaceConfig = config[master.toString(true /* skip encoding */)]; - if (workspaceConfig) { - const additionalFolders = workspaceConfig.folders - .map(f => URI.parse(f)) - .filter(r => r.scheme === Schemas.file); // only support files for now - - configuredFolders.push(...additionalFolders); - } - } - - // Remove duplicates - configuredFolders = distinct(configuredFolders, r => r.toString()); - - // Find changes - const changed = !equals(this.workspace.roots, configuredFolders, (r1, r2) => r1.toString() === r2.toString()); - - if (changed) { - - this.workspace.roots = configuredFolders; - this.workspace.name = configuredFolders.map(root => basename(root.fsPath) || root.fsPath).join(', '); - - this._onDidChangeWorkspaceRoots.fire(); - } + this._configuration = new Configuration(new BaseConfiguration(new ConfigurationModel(), new ConfigurationModel()), new ConfigurationModel(), new StrictResourceMap>(), this.workspace); } public getLegacyWorkspace(): ILegacyWorkspace { @@ -239,6 +192,14 @@ export class WorkspaceConfigurationService extends Disposable implements IWorksp return !!this.workspace; } + public hasFolderWorkspace(): boolean { + return this.workspace && !this.workspace.configuration; + } + + public hasMultiFolderWorkspace(): boolean { + return this.workspace && !!this.workspace.configuration; + } + public getRoot(resource: URI): URI { return this.workspace ? this.workspace.getRoot(resource) : null; } @@ -259,12 +220,22 @@ export class WorkspaceConfigurationService extends Disposable implements IWorksp return this.workspace ? this.legacyWorkspace.toResource(workspaceRelativePath) : null; } - public getConfigurationData(): IConfigurationData { - return this._configuration.toData(); + public initialize(trigger: boolean = true): TPromise { + this.resetCaches(); + return this.updateConfiguration() + .then(() => { + if (trigger) { + this.triggerConfigurationChange(); + } + }); } - public get configuration(): BaseConfiguration { - return this._configuration; + public reloadConfiguration(section?: string): TPromise { + return TPromise.as(this.getConfiguration(section)); + } + + public getConfigurationData(): IConfigurationData { + return this._configuration.toData(); } public getConfiguration(section?: string, overrides?: IConfigurationOverrides): C { @@ -284,46 +255,206 @@ export class WorkspaceConfigurationService extends Disposable implements IWorksp } public getUnsupportedWorkspaceKeys(): string[] { - return this.workspace ? this._configuration.getFolderConfigurationModel(this.workspace.roots[0]).workspaceSettingsConfig.unsupportedKeys : []; + return []; + } + + public isInWorkspaceContext(): boolean { + return false; + } + + protected triggerConfigurationChange(): void { + this._onDidUpdateConfiguration.fire({ source: ConfigurationSource.Workspace, sourceConfig: void 0 }); + } + + public handleWorkspaceFileEvents(event: FileChangesEvent): void { + // implemented by sub classes + } + + protected resetCaches(): void { + // implemented by sub classes + } + + protected updateConfiguration(): TPromise { + // implemented by sub classes + return TPromise.as(false); + } +} + +export class EmptyWorkspaceServiceImpl extends WorkspaceService { + + private baseConfigurationService: GlobalConfigurationService; + + constructor(environmentService: IEnvironmentService) { + super(); + this.baseConfigurationService = this._register(new GlobalConfigurationService(environmentService)); + this._register(this.baseConfigurationService.onDidUpdateConfiguration(e => this.onBaseConfigurationChanged(e))); + this.resetCaches(); } public reloadConfiguration(section?: string): TPromise { const current = this._configuration; - return this.baseConfigurationService.reloadConfiguration() .then(() => this.initialize(false)) // Reinitialize to ensure we are hitting the disk - .then(() => !this._configuration.equals(current)) // Check if the configuration is changed - .then(changed => changed ? this.trigger(ConfigurationSource.Workspace, ) : void 0) // Trigger event if changed - .then(() => this.getConfiguration(section)); - } - - public handleWorkspaceFileEvents(event: FileChangesEvent): void { - if (this.workspace) { - TPromise.join(this.workspace.roots.map(folder => this.cachedFolderConfigs.get(folder).handleWorkspaceFileEvents(event))) // handle file event for each folder - .then(folderConfigurations => - folderConfigurations.map((configuration, index) => ({ configuration, folder: this.workspace.roots[index] })) - .filter(folderConfiguration => !!folderConfiguration.configuration) // Filter folders which are not impacted by events - .map(folderConfiguration => this.updateFolderConfiguration(folderConfiguration.folder, folderConfiguration.configuration, true)) // Update the configuration of impacted folders - .reduce((result, value) => result || value, false)) // Check if the effective configuration of folder is changed - .then(changed => changed ? this.trigger(ConfigurationSource.Workspace) : void 0); // Trigger event if changed - } - } - - public initialize(trigger: boolean = true): TPromise { - this.initCaches(); - return this.doInitialize(this.workspace ? this.workspace.roots : []) .then(() => { - if (trigger) { - this.trigger(this.workspace ? ConfigurationSource.Workspace : ConfigurationSource.User); + // Check and trigger + if (!this._configuration.equals(current)) { + this.triggerConfigurationChange(); } + return super.reloadConfiguration(section); }); } - private onRootsChanged(): void { + private onBaseConfigurationChanged({ source, sourceConfig }: IConfigurationServiceEvent): void { + if (this._configuration.updateBaseConfiguration(this.baseConfigurationService.configuration())) { + this._onDidUpdateConfiguration.fire({ source, sourceConfig }); + } + } + + protected resetCaches(): void { + this._configuration = new Configuration(this.baseConfigurationService.configuration(), new ConfigurationModel(), new StrictResourceMap>(), null); + } + + protected triggerConfigurationChange(): void { + this._onDidUpdateConfiguration.fire({ source: ConfigurationSource.User, sourceConfig: this._configuration.user.contents }); + } +} + +export class WorkspaceServiceImpl extends WorkspaceService { + + public _serviceBrand: any; + + private baseConfigurationService: GlobalConfigurationService; + private workspaceConfiguration: WorkspaceConfiguration; + private cachedFolderConfigs: StrictResourceMap>; + + constructor(private workspaceConfigPath: string, private folderPath: string, private environmentService: IEnvironmentService, private workspaceSettingsRootFolder: string = WORKSPACE_CONFIG_FOLDER_DEFAULT_NAME) { + super(); + this.baseConfigurationService = this._register(new GlobalConfigurationService(environmentService)); + } + + public getUnsupportedWorkspaceKeys(): string[] { + return this.workspaceConfiguration ? [] : this._configuration.getFolderConfigurationModel(this.workspace.roots[0]).workspaceSettingsConfig.unsupportedKeys; + } + + public initialize(trigger: boolean = true): TPromise { if (!this.workspace) { + return this.initializeWorkspace() + .then(() => super.initialize(trigger)); + } + + if (this.workspaceConfiguration) { + return this.workspaceConfiguration.load() + .then(() => super.initialize(trigger)); + } + + return super.initialize(trigger); + } + + public reloadConfiguration(section?: string): TPromise { + const current = this._configuration; + return this.baseConfigurationService.reloadConfiguration() + .then(() => this.initialize(false)) // Reinitialize to ensure we are hitting the disk + .then(() => { + // Check and trigger + if (!this._configuration.equals(current)) { + this.triggerConfigurationChange(); + } + return super.reloadConfiguration(section); + }); + } + + public handleWorkspaceFileEvents(event: FileChangesEvent): void { + TPromise.join(this.workspace.roots.map(folder => this.cachedFolderConfigs.get(folder).handleWorkspaceFileEvents(event))) // handle file event for each folder + .then(folderConfigurations => + folderConfigurations.map((configuration, index) => ({ configuration, folder: this.workspace.roots[index] })) + .filter(folderConfiguration => !!folderConfiguration.configuration) // Filter folders which are not impacted by events + .map(folderConfiguration => this.updateFolderConfiguration(folderConfiguration.folder, folderConfiguration.configuration, true)) // Update the configuration of impacted folders + .reduce((result, value) => result || value, false)) // Check if the effective configuration of folder is changed + .then(changed => changed ? this.triggerConfigurationChange() : void 0); // Trigger event if changed + } + + protected resetCaches(): void { + this.cachedFolderConfigs = new StrictResourceMap>(); + this._configuration = new Configuration(this.baseConfigurationService.configuration(), new ConfigurationModel(), new StrictResourceMap>(), this.workspace); + this.initCachesForFolders(this.workspace.roots); + } + + private initializeWorkspace(): TPromise { + return (this.workspaceConfigPath ? this.initializeMulitFolderWorkspace() : this.initializeSingleFolderWorkspace()) + .then(() => { + this._register(this.baseConfigurationService.onDidUpdateConfiguration(e => this.onBaseConfigurationChanged(e))); + }); + } + + private initializeMulitFolderWorkspace(): TPromise { + this.workspaceConfiguration = this._register(new WorkspaceConfiguration(URI.file(this.workspaceConfigPath))); + return this.workspaceConfiguration.load() + .then(() => { + const workspaceConfigurationModel = this.workspaceConfiguration.workspaceConfigurationModel; + if (!workspaceConfigurationModel.id || !workspaceConfigurationModel.folders.length) { + return TPromise.wrapError(new Error('Invalid workspace configuraton file ' + this.workspaceConfigPath)); + } + const workspaceName = getWorkspaceLabel(this.environmentService, { id: workspaceConfigurationModel.id, configPath: this.workspaceConfiguration.workspaceConfigurationPath.fsPath }); + this.workspace = new Workspace(workspaceConfigurationModel.id, workspaceName, workspaceConfigurationModel.folders, this.workspaceConfiguration.workspaceConfigurationPath); + this.legacyWorkspace = new LegacyWorkspace(this.workspace.roots[0]); + this._register(this.workspaceConfiguration.onDidUpdateConfiguration(() => this.onWorkspaceConfigurationChanged())); + return null; + }); + } + + private initializeSingleFolderWorkspace(): TPromise { + return stat(this.folderPath) + .then(workspaceStat => { + const ctime = isLinux ? workspaceStat.ino : workspaceStat.birthtime.getTime(); // On Linux, birthtime is ctime, so we cannot use it! We use the ino instead! + const id = createHash('md5').update(this.folderPath).update(ctime ? String(ctime) : '').digest('hex'); + const folder = URI.file(this.folderPath); + this.workspace = new Workspace(id, paths.basename(this.folderPath), [folder], null); + this.legacyWorkspace = new LegacyWorkspace(folder, ctime); + return TPromise.as(null); + }); + } + + private initCachesForFolders(folders: URI[]): void { + for (const folder of folders) { + this.cachedFolderConfigs.set(folder, this._register(new FolderConfiguration(folder, this.workspaceSettingsRootFolder, this.workspaceConfiguration ? ConfigurationScope.FOLDER : ConfigurationScope.WORKSPACE))); + this.updateFolderConfiguration(folder, new FolderConfigurationModel(new FolderSettingsModel(null), [], ConfigurationScope.FOLDER), false); + } + } + + protected updateConfiguration(folders: URI[] = this.workspace.roots): TPromise { + return TPromise.join([...folders.map(folder => this.cachedFolderConfigs.get(folder).loadConfiguration() + .then(configuration => this.updateFolderConfiguration(folder, configuration, true)))]) + .then(changed => changed.reduce((result, value) => result || value, false)) + .then(changed => this.updateWorkspaceConfiguration(true) || changed); + } + + private onBaseConfigurationChanged({ source, sourceConfig }: IConfigurationServiceEvent): void { + if (source === ConfigurationSource.Default) { + this.workspace.roots.forEach(folder => this._configuration.getFolderConfigurationModel(folder).update()); + } + if (this._configuration.updateBaseConfiguration(this.baseConfigurationService.configuration())) { + this._onDidUpdateConfiguration.fire({ source, sourceConfig }); + } + } + + private onWorkspaceConfigurationChanged(): void { + let configuredFolders = this.workspaceConfiguration.workspaceConfigurationModel.folders; + const foldersChanged = !equals(this.workspace.roots, configuredFolders, (r1, r2) => r1.toString() === r2.toString()); + if (foldersChanged) { + this.workspace.roots = configuredFolders; + this.workspace.name = configuredFolders.map(root => basename(root.fsPath) || root.fsPath).join(', '); + this._onDidChangeWorkspaceRoots.fire(); + this.onFoldersChanged(); return; } + const configurationChanged = this.updateWorkspaceConfiguration(true); + if (configurationChanged) { + this.triggerConfigurationChange(); + } + } + + private onFoldersChanged(): void { let configurationChanged = false; // Remove the configurations of deleted folders @@ -340,69 +471,74 @@ export class WorkspaceConfigurationService extends Disposable implements IWorksp const toInitialize = this.workspace.roots.filter(folder => !this.cachedFolderConfigs.has(folder)); if (toInitialize.length) { this.initCachesForFolders(toInitialize); - this.doInitialize(toInitialize) + this.updateConfiguration(toInitialize) .then(changed => configurationChanged || changed) - .then(changed => changed ? this.trigger(ConfigurationSource.Workspace) : void 0); + .then(changed => changed ? this.triggerConfigurationChange() : void 0); } } - private initCaches(): void { - this.cachedFolderConfigs = new StrictResourceMap>(); - this._configuration = new Configuration(this.baseConfigurationService.configuration(), new ConfigurationModel(), new StrictResourceMap>(), this.workspace); - this.initCachesForFolders(this.workspace ? this.workspace.roots : []); - } - - private initCachesForFolders(folders: URI[]): void { - for (const folder of folders) { - this.cachedFolderConfigs.set(folder, this._register(new FolderConfiguration(folder, this.workspaceSettingsRootFolder, this.workspace))); - this.updateFolderConfiguration(folder, new FolderConfigurationModel(new FolderSettingsModel(null), [], ConfigurationScope.FOLDER), false); - } - } - - private doInitialize(folders: URI[]): TPromise { - return TPromise.join(folders.map(folder => this.cachedFolderConfigs.get(folder).loadConfiguration() - .then(configuration => this.updateFolderConfiguration(folder, configuration, true)))) - .then(changed => changed.reduce((result, value) => result || value, false)) - .then(changed => this.updateWorkspaceConfiguration(true) || changed); - } - - private onBaseConfigurationChanged(event: IConfigurationServiceEvent): void { - if (event.source === ConfigurationSource.Default) { - if (this.workspace) { - this.workspace.roots.forEach(folder => this._configuration.getFolderConfigurationModel(folder).update()); - } - } - - if (this._configuration.updateBaseConfiguration(this.baseConfigurationService.configuration())) { - this.trigger(event.source, event.sourceConfig); - } - } - - private trigger(source: ConfigurationSource, sourceConfig?: any): void { - if (!sourceConfig) { - sourceConfig = this.workspace ? this._configuration.getFolderConfigurationModel(this.workspace.roots[0]).contents : this._configuration.user.contents; - } - this._onDidUpdateConfiguration.fire({ source, sourceConfig }); - } - private updateFolderConfiguration(folder: URI, folderConfiguration: FolderConfigurationModel, compare: boolean): boolean { - let configurationChanged = false; - if (this.workspace) { - configurationChanged = this._configuration.updateFolderConfiguration(folder, folderConfiguration, compare); - if (this.workspace.roots[0].fsPath === folder.fsPath) { - // Workspace configuration changed - configurationChanged = this.updateWorkspaceConfiguration(compare) || configurationChanged; - } + let configurationChanged = this._configuration.updateFolderConfiguration(folder, folderConfiguration, compare); + if (!this.workspaceConfiguration) { + // Workspace configuration changed + configurationChanged = this.updateWorkspaceConfiguration(compare) || configurationChanged; } return configurationChanged; } private updateWorkspaceConfiguration(compare: boolean): boolean { - if (this.workspace) { - const firstFolderConfigurationModel = this._configuration.getFolderConfigurationModel(this.workspace.roots[0]); - return this._configuration.updateWorkspaceConfiguration(this.workspace.roots.length === 1 ? firstFolderConfigurationModel : firstFolderConfigurationModel.workspaceSettingsConfig.createWorkspaceConfigurationModel(), compare); + const workspaceConfiguration = this.workspaceConfiguration ? this.workspaceConfiguration.workspaceConfigurationModel.workspaceConfiguration : this._configuration.getFolderConfigurationModel(this.workspace.roots[0]); + return this._configuration.updateWorkspaceConfiguration(workspaceConfiguration, compare); + } + + protected triggerConfigurationChange(): void { + this._onDidUpdateConfiguration.fire({ source: ConfigurationSource.Workspace, sourceConfig: this._configuration.getFolderConfigurationModel(this.workspace.roots[0]).contents }); + } +} + +class WorkspaceConfiguration extends Disposable { + + private _workspaceConfigurationWatcher: ConfigWatcher>; + + private _onDidUpdateConfiguration: Emitter = this._register(new Emitter()); + public readonly onDidUpdateConfiguration: Event = this._onDidUpdateConfiguration.event; + + constructor(public readonly workspaceConfigurationPath: URI) { + super(); + } + + load(): TPromise { + if (!this.workspaceConfigurationPath) { + return TPromise.as(null); } - return false; + + if (this._workspaceConfigurationWatcher) { + return this._reload(); + } + + return new TPromise((c, e) => { + this._workspaceConfigurationWatcher = new ConfigWatcher(this.workspaceConfigurationPath.fsPath, { + changeBufferDelay: 300, defaultConfig: new WorkspaceConfigurationModel(null, this.workspaceConfigurationPath.fsPath), parse: (content: string, parseErrors: any[]) => { + const workspaceConfigurationModel = new WorkspaceConfigurationModel(content, this.workspaceConfigurationPath.fsPath); + parseErrors = [...workspaceConfigurationModel.errors]; + return workspaceConfigurationModel; + }, initCallback: () => c(null) + }); + this._register(toDisposable(() => this._workspaceConfigurationWatcher.dispose())); + this._register(this._workspaceConfigurationWatcher.onDidUpdateConfiguration(() => this._onDidUpdateConfiguration.fire())); + }); + } + + get workspaceConfigurationModel(): WorkspaceConfigurationModel { + return this._workspaceConfigurationWatcher ? this._workspaceConfigurationWatcher.getConfig() : new WorkspaceConfigurationModel(); + } + + private _reload(): TPromise { + return new TPromise(c => { + this._workspaceConfigurationWatcher.reload(() => { + c(null); + }); + }); } } @@ -416,7 +552,7 @@ class FolderConfiguration extends Disposable { private reloadConfigurationScheduler: RunOnceScheduler; private reloadConfigurationEventEmitter: Emitter> = new Emitter>(); - constructor(private folder: URI, private configFolderRelativePath: string, private workspace: Workspace) { + constructor(private folder: URI, private configFolderRelativePath: string, private scope: ConfigurationScope) { super(); this.workspaceFilePathToConfiguration = Object.create(null); @@ -424,16 +560,12 @@ class FolderConfiguration extends Disposable { } loadConfiguration(): TPromise> { - if (!this.workspace) { - return TPromise.wrap(new FolderConfigurationModel(new FolderSettingsModel(null), [], ConfigurationScope.FOLDER)); - } - // Load workspace locals return this.loadWorkspaceConfigFiles().then(workspaceConfigFiles => { // Consolidate (support *.json files in the workspace settings folder) const workspaceSettingsConfig = >workspaceConfigFiles[WORKSPACE_CONFIG_DEFAULT_PATH] || new FolderSettingsModel(null); const otherConfigModels = Object.keys(workspaceConfigFiles).filter(key => key !== WORKSPACE_CONFIG_DEFAULT_PATH).map(key => >workspaceConfigFiles[key]); - return new FolderConfigurationModel(workspaceSettingsConfig, otherConfigModels, this.workspace.roots.length === 1 ? ConfigurationScope.WORKSPACE : ConfigurationScope.FOLDER); + return new FolderConfigurationModel(workspaceSettingsConfig, otherConfigModels, this.scope); }); } @@ -465,10 +597,6 @@ class FolderConfiguration extends Disposable { } public handleWorkspaceFileEvents(event: FileChangesEvent): TPromise> { - if (!this.workspace) { - return TPromise.wrap(null); - } - const events = event.changes; let affectedByChanges = false; @@ -605,13 +733,13 @@ function resolveStat(resource: URI): TPromise { }); } -class Configuration extends BaseConfiguration { +export class Configuration extends BaseConfiguration { - constructor(private _baseConfiguration: Configuration, workspaceConfiguration: ConfigurationModel, protected folders: StrictResourceMap>, workspace: Workspace) { + constructor(private _baseConfiguration: BaseConfiguration, workspaceConfiguration: ConfigurationModel, protected folders: StrictResourceMap>, workspace: Workspace) { super(_baseConfiguration.defaults, _baseConfiguration.user, workspaceConfiguration, folders, workspace); } - updateBaseConfiguration(baseConfiguration: Configuration): boolean { + updateBaseConfiguration(baseConfiguration: BaseConfiguration): boolean { const current = new Configuration(this._baseConfiguration, this._workspaceConfiguration, this.folders, this._workspace); this._defaults = baseConfiguration.defaults; diff --git a/src/vs/workbench/services/configuration/node/configurationEditingService.ts b/src/vs/workbench/services/configuration/node/configurationEditingService.ts index ba58a32aee0..f6da2cfebb7 100644 --- a/src/vs/workbench/services/configuration/node/configurationEditingService.ts +++ b/src/vs/workbench/services/configuration/node/configurationEditingService.ts @@ -34,9 +34,9 @@ import { IChoiceService, IMessageService, Severity } from 'vs/platform/message/c import { ICommandService } from 'vs/platform/commands/common/commands'; interface IConfigurationEditOperation extends IConfigurationValue { + jsonPath: json.JSONPath; resource: URI; isWorkspaceStandalone?: boolean; - overrideIdentifier?: string; } interface IValidationResult { @@ -187,10 +187,10 @@ export class ConfigurationEditingService implements IConfigurationEditingService private getEdits(model: editorCommon.IModel, edit: IConfigurationEditOperation): Edit[] { const { tabSize, insertSpaces } = model.getOptions(); const eol = model.getEOL(); - const { key, value, overrideIdentifier } = edit; + const { value, jsonPath } = edit; - // Without key, the entire settings file is being replaced, so we just use JSON.stringify - if (!key) { + // Without jsonPath, the entire configuration file is being replaced, so we just use JSON.stringify + if (!jsonPath.length) { const content = JSON.stringify(value, null, insertSpaces ? strings.repeat(' ', tabSize) : '\t'); return [{ content, @@ -199,7 +199,7 @@ export class ConfigurationEditingService implements IConfigurationEditingService }]; } - return setProperty(model.getValue(), overrideIdentifier ? [keyFromOverrideIdentifier(overrideIdentifier), key] : [key], value, { tabSize, insertSpaces, eol }); + return setProperty(model.getValue(), jsonPath, value, { tabSize, insertSpaces, eol }); } private resolveModelReference(resource: URI): TPromise> { @@ -253,49 +253,59 @@ export class ConfigurationEditingService implements IConfigurationEditingService if (checkDirty && this.textFileService.isDirty(operation.resource)) { return this.wrapError(ConfigurationEditingErrorCode.ERROR_CONFIGURATION_FILE_DIRTY, target); } - return reference; + return TPromise.wrap(reference); }); } private getConfigurationEditOperation(target: ConfigurationTarget, config: IConfigurationValue, overrides: IConfigurationOverrides): IConfigurationEditOperation { + const workspace = this.contextService.getWorkspace(); + // Check for standalone workspace configurations if (config.key) { const standaloneConfigurationKeys = Object.keys(WORKSPACE_STANDALONE_CONFIGURATIONS); for (let i = 0; i < standaloneConfigurationKeys.length; i++) { const key = standaloneConfigurationKeys[i]; - const resource = this.contextService.toResource(WORKSPACE_STANDALONE_CONFIGURATIONS[key]); // TODO@Sandeep (https://github.com/Microsoft/vscode/issues/29456) + const resource = this.getConfigurationFileResource(WORKSPACE_STANDALONE_CONFIGURATIONS[key], overrides.resource); // Check for prefix if (config.key === key) { - return { key: '', value: config.value, resource, isWorkspaceStandalone: true }; + const jsonPath = workspace && workspace.configuration && resource && workspace.configuration.fsPath === resource.fsPath ? [key] : []; + return { key: jsonPath[jsonPath.length - 1], jsonPath, value: config.value, resource, isWorkspaceStandalone: true }; } // Check for prefix. const keyPrefix = `${key}.`; if (config.key.indexOf(keyPrefix) === 0) { - return { key: config.key.substr(keyPrefix.length), value: config.value, resource, isWorkspaceStandalone: true }; + const jsonPath = workspace && workspace.configuration && resource && workspace.configuration.fsPath === resource.fsPath ? [key, config.key.substr(keyPrefix.length)] : [config.key.substr(keyPrefix.length)]; + return { key: jsonPath[jsonPath.length - 1], jsonPath, value: config.value, resource, isWorkspaceStandalone: true }; } } } + let key = config.key; + let jsonPath = overrides.overrideIdentifier ? [keyFromOverrideIdentifier(overrides.overrideIdentifier), key] : [key]; if (target === ConfigurationTarget.USER) { - return { key: config.key, value: config.value, overrideIdentifier: overrides.overrideIdentifier, resource: URI.file(this.environmentService.appSettingsPath) }; + return { key, jsonPath, value: config.value, resource: URI.file(this.environmentService.appSettingsPath) }; } - return { key: config.key, value: config.value, overrideIdentifier: overrides.overrideIdentifier, resource: this.getConfigurationFileResource(overrides.resource) }; + const resource = this.getConfigurationFileResource(WORKSPACE_CONFIG_DEFAULT_PATH, overrides.resource); + if (workspace && workspace.configuration && resource && workspace.configuration.fsPath === resource.fsPath) { + jsonPath = ['settings', ...jsonPath]; + } + return { key, jsonPath, value: config.value, resource }; } - private getConfigurationFileResource(resource: URI): URI { + private getConfigurationFileResource(relativePath: string, resource: URI): URI { const workspace = this.contextService.getWorkspace(); if (workspace) { if (resource) { const root = this.contextService.getRoot(resource); if (root) { - return this.toResource(WORKSPACE_CONFIG_DEFAULT_PATH, root); + return this.toResource(relativePath, root); } } - return this.toResource(WORKSPACE_CONFIG_DEFAULT_PATH, workspace.roots[0]); + return workspace.configuration || this.toResource(relativePath, workspace.roots[0]); } return null; } diff --git a/src/vs/workbench/services/configuration/node/jsonEditingService.ts b/src/vs/workbench/services/configuration/node/jsonEditingService.ts new file mode 100644 index 00000000000..da7afec3203 --- /dev/null +++ b/src/vs/workbench/services/configuration/node/jsonEditingService.ts @@ -0,0 +1,134 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as nls from 'vs/nls'; +import { TPromise } from 'vs/base/common/winjs.base'; +import URI from 'vs/base/common/uri'; +import * as json from 'vs/base/common/json'; +import * as encoding from 'vs/base/node/encoding'; +import * as strings from 'vs/base/common/strings'; +import { setProperty } from 'vs/base/common/jsonEdit'; +import { Queue } from 'vs/base/common/async'; +import { Edit } from 'vs/base/common/jsonFormatter'; +import { IReference } from 'vs/base/common/lifecycle'; +import * as editorCommon from 'vs/editor/common/editorCommon'; +import { EditOperation } from 'vs/editor/common/core/editOperation'; +import { Range } from 'vs/editor/common/core/range'; +import { Selection } from 'vs/editor/common/core/selection'; +import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { IFileService } from 'vs/platform/files/common/files'; +import { ITextModelService, ITextEditorModel } from 'vs/editor/common/services/resolverService'; +import { IJSONEditingService, IJSONValue, JSONEditingError, JSONEditingErrorCode } from 'vs/workbench/services/configuration/common/jsonEditing'; + +export class JSONEditingService implements IJSONEditingService { + + public _serviceBrand: any; + + private queue: Queue; + + constructor( + @IFileService private fileService: IFileService, + @ITextModelService private textModelResolverService: ITextModelService, + @ITextFileService private textFileService: ITextFileService + ) { + this.queue = new Queue(); + } + + write(resource: URI, value: IJSONValue, save: boolean): TPromise { + return this.queue.queue(() => this.doWriteConfiguration(resource, value, save)); // queue up writes to prevent race conditions + } + + private doWriteConfiguration(resource: URI, value: IJSONValue, save: boolean): TPromise { + return this.resolveAndValidate(resource, save) + .then(reference => this.writeToBuffer(reference.object.textEditorModel, value)); + } + + private writeToBuffer(model: editorCommon.IModel, value: IJSONValue): TPromise { + const edit = this.getEdits(model, value)[0]; + if (this.applyEditsToBuffer(edit, model)) { + return this.textFileService.save(model.uri); + } + return TPromise.as(null); + } + + private applyEditsToBuffer(edit: Edit, model: editorCommon.IModel): boolean { + const startPosition = model.getPositionAt(edit.offset); + const endPosition = model.getPositionAt(edit.offset + edit.length); + const range = new Range(startPosition.lineNumber, startPosition.column, endPosition.lineNumber, endPosition.column); + let currentText = model.getValueInRange(range); + if (edit.content !== currentText) { + const editOperation = currentText ? EditOperation.replace(range, edit.content) : EditOperation.insert(startPosition, edit.content); + model.pushEditOperations([new Selection(startPosition.lineNumber, startPosition.column, startPosition.lineNumber, startPosition.column)], [editOperation], () => []); + return true; + } + return false; + } + + private getEdits(model: editorCommon.IModel, configurationValue: IJSONValue): Edit[] { + const { tabSize, insertSpaces } = model.getOptions(); + const eol = model.getEOL(); + const { key, value } = configurationValue; + + // Without key, the entire settings file is being replaced, so we just use JSON.stringify + if (!key) { + const content = JSON.stringify(value, null, insertSpaces ? strings.repeat(' ', tabSize) : '\t'); + return [{ + content, + length: content.length, + offset: 0 + }]; + } + + return setProperty(model.getValue(), [key], value, { tabSize, insertSpaces, eol }); + } + + private resolveModelReference(resource: URI): TPromise> { + return this.fileService.existsFile(resource) + .then(exists => { + const result = exists ? TPromise.as(null) : this.fileService.updateContent(resource, '{}', { encoding: encoding.UTF8 }); + return result.then(() => this.textModelResolverService.createModelReference(resource)); + }); + } + + private hasParseErrors(model: editorCommon.IModel): boolean { + const parseErrors: json.ParseError[] = []; + json.parse(model.getValue(), parseErrors, { allowTrailingComma: true }); + return parseErrors.length > 0; + } + + private resolveAndValidate(resource: URI, checkDirty: boolean): TPromise> { + return this.resolveModelReference(resource) + .then(reference => { + const model = reference.object.textEditorModel; + + if (this.hasParseErrors(model)) { + return this.wrapError>(JSONEditingErrorCode.ERROR_INVALID_FILE); + } + + // Target cannot be dirty if not writing into buffer + if (checkDirty && this.textFileService.isDirty(resource)) { + return this.wrapError>(JSONEditingErrorCode.ERROR_FILE_DIRTY); + } + return reference; + }); + } + + private wrapError(code: JSONEditingErrorCode): TPromise { + const message = this.toErrorMessage(code); + return TPromise.wrapError(new JSONEditingError(message, code)); + } + + private toErrorMessage(error: JSONEditingErrorCode): string { + switch (error) { + // User issues + case JSONEditingErrorCode.ERROR_INVALID_FILE: { + return nls.localize('errorInvalidFile', "Unable to write into the file. Please open the file to correct errors/warnings in the file and try again."); + }; + case JSONEditingErrorCode.ERROR_FILE_DIRTY: { + return nls.localize('errorFileDirty', "Unable to write into the file because the file is dirty. Please save the file and try again."); + }; + } + } +} \ No newline at end of file diff --git a/src/vs/workbench/services/configuration/test/node/configuration.test.ts b/src/vs/workbench/services/configuration/test/node/configuration.test.ts index fadc195a846..ca351ad50ba 100644 --- a/src/vs/workbench/services/configuration/test/node/configuration.test.ts +++ b/src/vs/workbench/services/configuration/test/node/configuration.test.ts @@ -13,13 +13,12 @@ import * as sinon from 'sinon'; import { TPromise } from 'vs/base/common/winjs.base'; import { Registry } from 'vs/platform/registry/common/platform'; import { ParsedArgs } from 'vs/platform/environment/common/environment'; -import { Workspace } from 'vs/platform/workspace/common/workspace'; import { EnvironmentService } from 'vs/platform/environment/node/environmentService'; import { parseArgs } from 'vs/platform/environment/node/argv'; import extfs = require('vs/base/node/extfs'); import uuid = require('vs/base/common/uuid'); import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry'; -import { WorkspaceConfigurationService } from 'vs/workbench/services/configuration/node/configuration'; +import { WorkspaceServiceImpl, WorkspaceService } from 'vs/workbench/services/configuration/node/configuration'; import URI from 'vs/base/common/uri'; import { FileChangeType, FileChangesEvent } from 'vs/platform/files/common/files'; @@ -46,9 +45,9 @@ suite('WorkspaceConfigurationService - Node', () => { }); } - function createService(workspaceDir: string, globalSettingsFile: string): TPromise { + function createService(workspaceDir: string, globalSettingsFile: string): TPromise { const environmentService = new SettingsTestEnvironmentService(parseArgs(process.argv), process.execPath, globalSettingsFile); - const service = new WorkspaceConfigurationService(environmentService, new Workspace(workspaceDir, workspaceDir, [URI.file(workspaceDir)])); + const service = new WorkspaceServiceImpl(null, workspaceDir, environmentService); return service.initialize().then(() => service); } @@ -203,10 +202,7 @@ suite('WorkspaceConfigurationService - Node', () => { test('workspace change triggers event', (done: () => void) => { createWorkspace((workspaceDir, globalSettingsFile, cleanUp) => { - const environmentService = new SettingsTestEnvironmentService(parseArgs(process.argv), process.execPath, globalSettingsFile); - const service = new WorkspaceConfigurationService(environmentService, new Workspace(workspaceDir, workspaceDir, [URI.file(workspaceDir)])); - - return service.initialize().then(() => { + return createService(workspaceDir, globalSettingsFile).then(service => { service.onDidUpdateConfiguration(event => { const config = service.getConfiguration<{ testworkbench: { editor: { icons: boolean } } }>(); assert.equal(config.testworkbench.editor.icons, true); @@ -228,10 +224,7 @@ suite('WorkspaceConfigurationService - Node', () => { test('workspace reload should triggers event if content changed', (done: () => void) => { createWorkspace((workspaceDir, globalSettingsFile, cleanUp) => { - const environmentService = new SettingsTestEnvironmentService(parseArgs(process.argv), process.execPath, globalSettingsFile); - const service = new WorkspaceConfigurationService(environmentService, new Workspace(workspaceDir, workspaceDir, [URI.file(workspaceDir)])); - - return service.initialize().then(() => { + return createService(workspaceDir, globalSettingsFile).then(service => { const settingsFile = path.join(workspaceDir, '.vscode', 'settings.json'); fs.writeFileSync(settingsFile, '{ "testworkbench.editor.icons": true }'); @@ -252,10 +245,7 @@ suite('WorkspaceConfigurationService - Node', () => { test('workspace reload should not trigger event if nothing changed', (done: () => void) => { createWorkspace((workspaceDir, globalSettingsFile, cleanUp) => { - const environmentService = new SettingsTestEnvironmentService(parseArgs(process.argv), process.execPath, globalSettingsFile); - const service = new WorkspaceConfigurationService(environmentService, new Workspace(workspaceDir, workspaceDir, [URI.file(workspaceDir)])); - - return service.initialize().then(() => { + return createService(workspaceDir, globalSettingsFile).then(service => { const settingsFile = path.join(workspaceDir, '.vscode', 'settings.json'); fs.writeFileSync(settingsFile, '{ "testworkbench.editor.icons": true }'); @@ -276,10 +266,7 @@ suite('WorkspaceConfigurationService - Node', () => { test('workspace reload should not trigger event if there is no model', (done: () => void) => { createWorkspace((workspaceDir, globalSettingsFile, cleanUp) => { - const environmentService = new SettingsTestEnvironmentService(parseArgs(process.argv), process.execPath, globalSettingsFile); - const service = new WorkspaceConfigurationService(environmentService, new Workspace(workspaceDir, workspaceDir, [URI.file(workspaceDir)])); - - return service.initialize().then(() => { + return createService(workspaceDir, globalSettingsFile).then(service => { const target = sinon.stub(); service.onDidUpdateConfiguration(event => target()); service.reloadConfiguration().done(() => { diff --git a/src/vs/workbench/services/configuration/test/node/configurationEditingService.test.ts b/src/vs/workbench/services/configuration/test/node/configurationEditingService.test.ts index bcf32d376e3..88d1fab6ac7 100644 --- a/src/vs/workbench/services/configuration/test/node/configurationEditingService.test.ts +++ b/src/vs/workbench/services/configuration/test/node/configurationEditingService.test.ts @@ -15,14 +15,13 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { Registry } from 'vs/platform/registry/common/platform'; import { ParsedArgs, IEnvironmentService } from 'vs/platform/environment/common/environment'; import { parseArgs } from 'vs/platform/environment/node/argv'; -import { IWorkspaceContextService, Workspace } from 'vs/platform/workspace/common/workspace'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { EnvironmentService } from 'vs/platform/environment/node/environmentService'; import extfs = require('vs/base/node/extfs'); import { TestTextFileService, TestEditorGroupService, TestLifecycleService, TestBackupFileService } from 'vs/workbench/test/workbenchTestServices'; import uuid = require('vs/base/common/uuid'); import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry'; -import { WorkspaceConfigurationService } from 'vs/workbench/services/configuration/node/configuration'; -import URI from 'vs/base/common/uri'; +import { WorkspaceService, EmptyWorkspaceServiceImpl, WorkspaceServiceImpl } from 'vs/workbench/services/configuration/node/configuration'; import { FileService } from 'vs/workbench/services/files/node/fileService'; import { ConfigurationEditingService } from 'vs/workbench/services/configuration/node/configurationEditingService'; import { ConfigurationTarget, ConfigurationEditingError, ConfigurationEditingErrorCode } from 'vs/workbench/services/configuration/common/configurationEditing'; @@ -116,8 +115,7 @@ suite('ConfigurationEditingService', () => { instantiationService = new TestInstantiationService(); const environmentService = new SettingsTestEnvironmentService(parseArgs(process.argv), process.execPath, globalSettingsFile); instantiationService.stub(IEnvironmentService, environmentService); - const workspace = noWorkspace ? null : new Workspace(workspaceDir, workspaceDir, [URI.file(workspaceDir)]); - const workspaceService = new WorkspaceConfigurationService(environmentService, workspace); + const workspaceService = noWorkspace ? new EmptyWorkspaceServiceImpl(environmentService) : new WorkspaceServiceImpl(null, workspaceDir, environmentService); instantiationService.stub(IWorkspaceContextService, workspaceService); instantiationService.stub(IConfigurationService, workspaceService); instantiationService.stub(ILifecycleService, new TestLifecycleService()); @@ -150,7 +148,7 @@ suite('ConfigurationEditingService', () => { function clearServices(): void { if (instantiationService) { - const configuraitonService = instantiationService.get(IConfigurationService); + const configuraitonService = instantiationService.get(IConfigurationService); if (configuraitonService) { configuraitonService.dispose(); } diff --git a/src/vs/workbench/services/files/node/watcher/nsfw/watcherService.ts b/src/vs/workbench/services/files/node/watcher/nsfw/watcherService.ts index be60fdf3bae..b3f8c608892 100644 --- a/src/vs/workbench/services/files/node/watcher/nsfw/watcherService.ts +++ b/src/vs/workbench/services/files/node/watcher/nsfw/watcherService.ts @@ -115,7 +115,7 @@ export class FileWatcher { return; } - // Emit through broadcast service + // Emit through event emitter if (events.length > 0) { this.onFileChanges(toFileChangesEvent(events)); } diff --git a/src/vs/workbench/services/files/node/watcher/unix/watcherService.ts b/src/vs/workbench/services/files/node/watcher/unix/watcherService.ts index fa895f465ea..0f96d2c3b9c 100644 --- a/src/vs/workbench/services/files/node/watcher/unix/watcherService.ts +++ b/src/vs/workbench/services/files/node/watcher/unix/watcherService.ts @@ -89,7 +89,7 @@ export class FileWatcher { return; } - // Emit through broadcast service + // Emit through event emitter if (events.length > 0) { this.onFileChanges(toFileChangesEvent(events)); } diff --git a/src/vs/workbench/services/files/node/watcher/win32/watcherService.ts b/src/vs/workbench/services/files/node/watcher/win32/watcherService.ts index 518a08f22a3..54ba8d297fd 100644 --- a/src/vs/workbench/services/files/node/watcher/win32/watcherService.ts +++ b/src/vs/workbench/services/files/node/watcher/win32/watcherService.ts @@ -55,7 +55,7 @@ export class FileWatcher { return; } - // Emit through broadcast service + // Emit through event emitter if (events.length > 0) { this.onFileChanges(toFileChangesEvent(events)); } diff --git a/src/vs/workbench/services/files/test/node/watcher.test.ts b/src/vs/workbench/services/files/test/node/watcher.test.ts index 023ebbb8843..a8c6b9ee80b 100644 --- a/src/vs/workbench/services/files/test/node/watcher.test.ts +++ b/src/vs/workbench/services/files/test/node/watcher.test.ts @@ -33,7 +33,7 @@ class TestFileWatcher { // Normalize let normalizedEvents = normalize(events); - // Emit through broadcast service + // Emit through event emitter if (normalizedEvents.length > 0) { this._onFileChanges.fire(toFileChangesEvent(normalizedEvents)); } diff --git a/src/vs/workbench/services/history/browser/history.ts b/src/vs/workbench/services/history/browser/history.ts index 1607663e439..0a61c7c71cc 100644 --- a/src/vs/workbench/services/history/browser/history.ts +++ b/src/vs/workbench/services/history/browser/history.ts @@ -395,7 +395,7 @@ export class HistoryService extends BaseHistoryService implements IHistoryServic this.removeFromHistory(arg1); this.removeFromStack(arg1); this.removeFromRecentlyClosedFiles(arg1); - this.removeFromRecentlyOpen(arg1); + this.removeFromRecentlyOpened(arg1); } private removeExcludedFromHistory(): void { @@ -568,14 +568,14 @@ export class HistoryService extends BaseHistoryService implements IHistoryServic this.recentlyClosedFiles = this.recentlyClosedFiles.filter(e => !this.matchesFile(e.resource, arg1)); } - private removeFromRecentlyOpen(arg1: IEditorInput | IResourceInput | FileChangesEvent): void { + private removeFromRecentlyOpened(arg1: IEditorInput | IResourceInput | FileChangesEvent): void { if (arg1 instanceof EditorInput || arg1 instanceof FileChangesEvent) { return; // for now do not delete from file events since recently open are likely out of workspace files for which there are no delete events } const input = arg1 as IResourceInput; - this.windowService.removeFromRecentlyOpen([input.resource.fsPath]); + this.windowService.removeFromRecentlyOpened([input.resource.fsPath]); } private isFileOpened(resource: URI, group: IEditorGroup): boolean { diff --git a/src/vs/workbench/services/search/node/rawSearchService.ts b/src/vs/workbench/services/search/node/rawSearchService.ts index c0f39726008..f2666cecaea 100644 --- a/src/vs/workbench/services/search/node/rawSearchService.ts +++ b/src/vs/workbench/services/search/node/rawSearchService.ts @@ -88,7 +88,7 @@ export class SearchService implements IRawSearchService { return this.doTextSearch(engine, SearchService.BATCH_SIZE); } - public doFileSearch(EngineClass: { new (config: IRawSearch): ISearchEngine; }, config: IRawSearch, batchSize?: number): PPromise { + public doFileSearch(EngineClass: { new(config: IRawSearch): ISearchEngine; }, config: IRawSearch, batchSize?: number): PPromise { if (config.sortByScore) { let sortedSearch = this.trySortedSearchFromCache(config); diff --git a/src/vs/workbench/services/themes/electron-browser/themeCompatibility.ts b/src/vs/workbench/services/themes/electron-browser/themeCompatibility.ts index 1bddeb71c6d..119c2eca316 100644 --- a/src/vs/workbench/services/themes/electron-browser/themeCompatibility.ts +++ b/src/vs/workbench/services/themes/electron-browser/themeCompatibility.ts @@ -66,7 +66,7 @@ addSettingMapping('findMatchHighlight', peekViewResultsMatchHighlight); addSettingMapping('referenceHighlight', peekViewEditorMatchHighlight); addSettingMapping('lineHighlight', editorColorRegistry.editorLineHighlight); addSettingMapping('rangeHighlight', editorColorRegistry.editorRangeHighlight); -addSettingMapping('caret', editorColorRegistry.editorCursor); +addSettingMapping('caret', editorColorRegistry.editorCursorForeground); addSettingMapping('invisibles', editorColorRegistry.editorWhitespaces); addSettingMapping('guide', editorColorRegistry.editorIndentGuides); diff --git a/src/vs/workbench/services/workspace/common/workspaceEditing.ts b/src/vs/workbench/services/workspace/common/workspaceEditing.ts index 357c5e0260f..174af8c45b4 100644 --- a/src/vs/workbench/services/workspace/common/workspaceEditing.ts +++ b/src/vs/workbench/services/workspace/common/workspaceEditing.ts @@ -14,6 +14,8 @@ export interface IWorkspaceEditingService { _serviceBrand: ServiceIdentifier; + createAndOpenWorkspace(roots: URI[]): TPromise; + addRoots(roots: URI[]): TPromise; removeRoots(roots: URI[]): TPromise; diff --git a/src/vs/workbench/services/workspace/node/workspaceEditingService.ts b/src/vs/workbench/services/workspace/node/workspaceEditingService.ts index 1e63ca75ccf..403a2c495d0 100644 --- a/src/vs/workbench/services/workspace/node/workspaceEditingService.ts +++ b/src/vs/workbench/services/workspace/node/workspaceEditingService.ts @@ -10,37 +10,31 @@ import URI from 'vs/base/common/uri'; import { equals, distinct } from 'vs/base/common/arrays'; import { TPromise } from "vs/base/common/winjs.base"; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { IConfigurationEditingService, ConfigurationTarget } from 'vs/workbench/services/configuration/common/configurationEditing'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IWindowsService } from 'vs/platform/windows/common/windows'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; - -interface IWorkspaceConfiguration { - [master: string]: { - folders: string[]; - }; -} - -const workspaceConfigKey = 'workspace'; +import { IJSONEditingService } from 'vs/workbench/services/configuration/common/jsonEditing'; +import { IWorkspacesService } from "vs/platform/workspaces/common/workspaces"; export class WorkspaceEditingService implements IWorkspaceEditingService { public _serviceBrand: any; constructor( - @IConfigurationEditingService private configurationEditingService: IConfigurationEditingService, - @IConfigurationService private configurationService: IConfigurationService, + @IJSONEditingService private jsonEditingService: IJSONEditingService, @IWorkspaceContextService private contextService: IWorkspaceContextService, - @IEnvironmentService private environmentService: IEnvironmentService + @IEnvironmentService private environmentService: IEnvironmentService, + @IWindowsService private windowsService: IWindowsService, + @IWorkspacesService private workspacesService: IWorkspacesService ) { } - private supported(): boolean { - if (!this.contextService.hasWorkspace()) { - return false; // we need a workspace to begin with + public createAndOpenWorkspace(roots: URI[]): TPromise { + const paths = this.validateRoots(roots); + if (paths.length) { + return this.workspacesService.createWorkspace(paths) + .then(newWorkspace => this.windowsService.openWindow([newWorkspace.configPath])); } - - // TODO@Ben multi root - return this.environmentService.appQuality !== 'stable'; // not yet enabled in stable + return TPromise.as(null); } public addRoots(rootsToAdd: URI[]): TPromise { @@ -64,13 +58,19 @@ export class WorkspaceEditingService implements IWorkspaceEditingService { return this.doSetRoots(roots.filter(root => rootsToRemoveRaw.indexOf(root.toString()) === -1)); } - private doSetRoots(newRoots: URI[]): TPromise { - const workspaceUserConfig = this.configurationService.lookup(workspaceConfigKey).user as IWorkspaceConfiguration || Object.create(null); - const master = this.contextService.getWorkspace().roots[0]; - const masterKey = master.toString(true /* skip encoding */); + private supported(): boolean { + if (this.contextService.hasMultiFolderWorkspace()) { + return false; // we need a multi folder workspace to begin with + } - const currentWorkspaceRoots = this.validateRoots(master, workspaceUserConfig[masterKey] && workspaceUserConfig[masterKey].folders); - const newWorkspaceRoots = this.validateRoots(master, newRoots); + // TODO@Ben multi root + return this.environmentService.appQuality !== 'stable'; // not yet enabled in stable + } + + private doSetRoots(newRoots: URI[]): TPromise { + const workspace = this.contextService.getWorkspace(); + const currentWorkspaceRoots = this.contextService.getWorkspace().roots.map(root => root.fsPath); + const newWorkspaceRoots = this.validateRoots(newRoots); // See if there are any changes if (equals(currentWorkspaceRoots, newWorkspaceRoots)) { @@ -79,30 +79,21 @@ export class WorkspaceEditingService implements IWorkspaceEditingService { // Apply to config if (newWorkspaceRoots.length) { - workspaceUserConfig[masterKey] = { - folders: newWorkspaceRoots - }; + return this.jsonEditingService.write(workspace.configuration, { key: 'folders', value: newWorkspaceRoots }, true); } else { - delete workspaceUserConfig[masterKey]; + // TODO: Sandeep - Removing all roots? } - return this.configurationEditingService.writeConfiguration(ConfigurationTarget.USER, { key: workspaceConfigKey, value: workspaceUserConfig }).then(() => void 0); + return TPromise.as(null); } - private validateRoots(master: URI, roots: URI[]): string[] { + private validateRoots(roots: URI[]): string[] { if (!roots) { return []; } // Prevent duplicates const validatedRoots = distinct(roots.map(root => root.toString(true /* skip encoding */))); - - // Make sure we do not set the master folder as root - const masterIndex = validatedRoots.indexOf(master.toString(true /* skip encoding */)); - if (masterIndex >= 0) { - validatedRoots.splice(masterIndex, 1); - } - return validatedRoots; } } \ No newline at end of file diff --git a/src/vs/workbench/test/browser/part.test.ts b/src/vs/workbench/test/browser/part.test.ts index 01eb8329c12..6eaae451336 100644 --- a/src/vs/workbench/test/browser/part.test.ts +++ b/src/vs/workbench/test/browser/part.test.ts @@ -89,7 +89,7 @@ suite('Workbench Part', () => { fixture = document.createElement('div'); fixture.id = fixtureId; document.body.appendChild(fixture); - storage = new StorageService(new InMemoryLocalStorage(), null, TestWorkspace); + storage = new StorageService(new InMemoryLocalStorage(), null, TestWorkspace.id); }); teardown(() => { diff --git a/src/vs/workbench/test/common/memento.test.ts b/src/vs/workbench/test/common/memento.test.ts index 4c5ecf1182c..686250e7b1b 100644 --- a/src/vs/workbench/test/common/memento.test.ts +++ b/src/vs/workbench/test/common/memento.test.ts @@ -17,7 +17,7 @@ suite('Workbench Memento', () => { let storage; setup(() => { - storage = new StorageService(new InMemoryLocalStorage(), null, TestWorkspace); + storage = new StorageService(new InMemoryLocalStorage(), null, TestWorkspace.id); }); test('Loading and Saving Memento with Scopes', () => { diff --git a/src/vs/workbench/test/electron-browser/api/extHostDocumentData.test.ts b/src/vs/workbench/test/electron-browser/api/extHostDocumentData.test.ts index 12e8380dfc3..a4c48b2f95a 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostDocumentData.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostDocumentData.test.ts @@ -269,6 +269,34 @@ suite('ExtHostDocumentData', () => { range = data.document.getWordRangeAtPosition(new Position(0, 11), /yy/); assert.equal(range, undefined); }); + + test('getWordRangeAtPosition doesn\'t quite use the regex as expected, #29102', function () { + data = new ExtHostDocumentData(undefined, URI.file(''), [ + 'some text here', + '/** foo bar */', + 'function() {', + ' "far boo"', + '}' + ], '\n', 'text', 1, false); + + let range = data.document.getWordRangeAtPosition(new Position(0, 0), /\/\*.+\*\//); + assert.equal(range, undefined); + + range = data.document.getWordRangeAtPosition(new Position(1, 0), /\/\*.+\*\//); + assert.equal(range.start.line, 1); + assert.equal(range.start.character, 0); + assert.equal(range.end.line, 1); + assert.equal(range.end.character, 14); + + range = data.document.getWordRangeAtPosition(new Position(3, 0), /("|').*\1/); + assert.equal(range, undefined); + + range = data.document.getWordRangeAtPosition(new Position(3, 1), /("|').*\1/); + assert.equal(range.start.line, 3); + assert.equal(range.start.character, 1); + assert.equal(range.end.line, 3); + assert.equal(range.end.character, 10); + }); }); enum AssertDocumentLineMappingDirection { 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 3cfcedbac4b..bdfb54052d7 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostLanguageFeatures.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostLanguageFeatures.test.ts @@ -650,10 +650,29 @@ suite('ExtHostLanguageFeatures', function () { assert.equal(value.length, 2); let [first, second] = value; - assert.equal(first.command.title, 'Testing1'); - assert.equal(first.command.id, 'test1'); - assert.equal(second.command.title, 'Testing2'); - assert.equal(second.command.id, 'test2'); + assert.equal(first.title, 'Testing1'); + assert.equal(first.id, 'test1'); + assert.equal(second.title, 'Testing2'); + assert.equal(second.id, 'test2'); + }); + }); + }); + + test('Cannot read property \'id\' of undefined, #29469', function () { + + disposables.push(extHost.registerCodeActionProvider(defaultSelector, { + provideCodeActions(): any { + return [ + undefined, + null, + { command: 'test', title: 'Testing' } + ]; + } + })); + + return threadService.sync().then(() => { + return getCodeActions(model, model.getFullModelRange()).then(value => { + assert.equal(value.length, 1); }); }); }); diff --git a/src/vs/workbench/test/electron-browser/api/extHostWorkspace.test.ts b/src/vs/workbench/test/electron-browser/api/extHostWorkspace.test.ts index ec43ae2a99e..d5429e7a612 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostWorkspace.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostWorkspace.test.ts @@ -70,35 +70,35 @@ suite('ExtHostWorkspace', function () { let ws = new ExtHostWorkspace(new TestThreadService(), { id: 'foo', name: 'Test', roots: [] }); let sub = ws.onDidChangeWorkspace(e => { - assert.deepEqual(e.addedFolders, []); - assert.deepEqual(e.removedFolders, []); + assert.deepEqual(e.added, []); + assert.deepEqual(e.removed, []); }); ws.$acceptWorkspaceData({ id: 'foo', name: 'Test', roots: [] }); sub.dispose(); sub = ws.onDidChangeWorkspace(e => { - assert.deepEqual(e.removedFolders, []); - assert.equal(e.addedFolders.length, 1); - assert.equal(e.addedFolders[0].toString(), 'foo:bar'); + assert.deepEqual(e.removed, []); + assert.equal(e.added.length, 1); + assert.equal(e.added[0].uri.toString(), 'foo:bar'); }); ws.$acceptWorkspaceData({ id: 'foo', name: 'Test', roots: [URI.parse('foo:bar')] }); sub.dispose(); sub = ws.onDidChangeWorkspace(e => { - assert.deepEqual(e.removedFolders, []); - assert.equal(e.addedFolders.length, 1); - assert.equal(e.addedFolders[0].toString(), 'foo:bar2'); + assert.deepEqual(e.removed, []); + assert.equal(e.added.length, 1); + assert.equal(e.added[0].uri.toString(), 'foo:bar2'); }); ws.$acceptWorkspaceData({ id: 'foo', name: 'Test', roots: [URI.parse('foo:bar'), URI.parse('foo:bar2')] }); sub.dispose(); sub = ws.onDidChangeWorkspace(e => { - assert.equal(e.removedFolders.length, 2); - assert.equal(e.removedFolders[0].toString(), 'foo:bar'); - assert.equal(e.removedFolders[1].toString(), 'foo:bar2'); + assert.equal(e.removed.length, 2); + assert.equal(e.removed[0].uri.toString(), 'foo:bar'); + assert.equal(e.removed[1].uri.toString(), 'foo:bar2'); - assert.equal(e.addedFolders.length, 1); - assert.equal(e.addedFolders[0].toString(), 'foo:bar3'); + assert.equal(e.added.length, 1); + assert.equal(e.added[0].uri.toString(), 'foo:bar3'); }); ws.$acceptWorkspaceData({ id: 'foo', name: 'Test', roots: [URI.parse('foo:bar3')] }); sub.dispose(); @@ -109,10 +109,10 @@ suite('ExtHostWorkspace', function () { let ws = new ExtHostWorkspace(new TestThreadService(), { id: 'foo', name: 'Test', roots: [] }); let sub = ws.onDidChangeWorkspace(e => { assert.throws(() => { - (e).addedFolders = []; + (e).added = []; }); assert.throws(() => { - (e.addedFolders)[0] = null; + (e.added)[0] = null; }); }); ws.$acceptWorkspaceData({ id: 'foo', name: 'Test', roots: [] }); diff --git a/src/vs/workbench/test/workbenchTestServices.ts b/src/vs/workbench/test/workbenchTestServices.ts index b88a6f173ea..c4e814f7220 100644 --- a/src/vs/workbench/test/workbenchTestServices.ts +++ b/src/vs/workbench/test/workbenchTestServices.ts @@ -27,7 +27,7 @@ import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { IEditorInput, IEditorOptions, Position, Direction, IEditor, IResourceInput, ITextEditorOptions } from 'vs/platform/editor/common/editor'; import { IUntitledEditorService, UntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; import { IMessageService, IConfirmation } from 'vs/platform/message/common/message'; -import { ILegacyWorkspace, IWorkspaceContextService, IWorkspace } from 'vs/platform/workspace/common/workspace'; +import { ILegacyWorkspace, IWorkspaceContextService, IWorkspace as IWorkbenchWorkspace } from 'vs/platform/workspace/common/workspace'; import { ILifecycleService, ShutdownEvent, ShutdownReason, StartupKind, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { EditorStacksModel } from 'vs/workbench/common/editor/editorStacksModel'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; @@ -54,6 +54,8 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { isLinux } from 'vs/base/common/platform'; import { generateUuid } from 'vs/base/common/uuid'; import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; +import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier } from "vs/platform/workspaces/common/workspaces"; +import { IRecentlyOpened } from "vs/platform/history/common/history"; export function createFileInput(instantiationService: IInstantiationService, resource: URI): FileEditorInput { return instantiationService.createInstance(FileEditorInput, resource, void 0); @@ -64,7 +66,7 @@ export const TestEnvironmentService = new EnvironmentService(parseArgs(process.a export class TestContextService implements IWorkspaceContextService { public _serviceBrand: any; - private workspace: IWorkspace; + private workspace: IWorkbenchWorkspace; private id: string; private options: any; @@ -89,11 +91,19 @@ export class TestContextService implements IWorkspaceContextService { return !!this.workspace; } + public hasFolderWorkspace(): boolean { + return this.hasWorkspace(); + } + + public hasMultiFolderWorkspace(): boolean { + return false; + } + public getLegacyWorkspace(): ILegacyWorkspace { return this.workspace ? { resource: this.workspace.roots[0] } : void 0; } - public getWorkspace(): IWorkspace { + public getWorkspace(): IWorkbenchWorkspace { return this.workspace; } @@ -397,7 +407,7 @@ export class TestStorageService extends EventEmitter implements IStorageService super(); let context = new TestContextService(); - this.storage = new StorageService(new InMemoryLocalStorage(), null, context.getWorkspace()); + this.storage = new StorageService(new InMemoryLocalStorage(), null, context.getWorkspace().id); } store(key: string, value: any, scope: StorageScope = StorageScope.GLOBAL): void { @@ -869,7 +879,7 @@ export class TestWindowService implements IWindowService { return TPromise.as(void 0); } - closeFolder(): TPromise { + closeWorkspace(): TPromise { return TPromise.as(void 0); } @@ -881,7 +891,7 @@ export class TestWindowService implements IWindowService { return TPromise.as(void 0); } - getRecentlyOpen(): TPromise<{ files: string[]; folders: string[]; }> { + getRecentlyOpened(): TPromise { return TPromise.as(void 0); } @@ -920,6 +930,10 @@ export class TestWindowService implements IWindowService { showSaveDialog(options: Electron.SaveDialogOptions, callback?: (fileName: string) => void): string { return void 0; } + + showOpenDialog(options: Electron.OpenDialogOptions, callback?: (fileNames: string[]) => void): string[] { + return void 0; + } } export class TestLifecycleService implements ILifecycleService { @@ -996,8 +1010,7 @@ export class TestWindowsService implements IWindowsService { return TPromise.as(void 0); } - // TODO@joao: rename, shouldn't this be closeWindow? - closeFolder(windowId: number): TPromise { + closeWorkspace(windowId: number): TPromise { return TPromise.as(void 0); } @@ -1009,19 +1022,19 @@ export class TestWindowsService implements IWindowsService { return TPromise.as(void 0); } - addToRecentlyOpen(paths: { path: string, isFile?: boolean }[]): TPromise { + addRecentlyOpened(files: string[]): TPromise { return TPromise.as(void 0); } - removeFromRecentlyOpen(paths: string[]): TPromise { + removeFromRecentlyOpened(arg1: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier)[]): TPromise { return TPromise.as(void 0); } - clearRecentPathsList(): TPromise { + clearRecentlyOpened(): TPromise { return TPromise.as(void 0); } - getRecentlyOpen(windowId: number): TPromise<{ files: string[]; folders: string[]; }> { + getRecentlyOpened(windowId: number): TPromise { return TPromise.as(void 0); } @@ -1082,7 +1095,7 @@ export class TestWindowsService implements IWindowsService { return TPromise.as(void 0); } - getWindows(): TPromise<{ id: number; path: string; title: string; }[]> { + getWindows(): TPromise<{ id: number; workspace?: IWorkspaceIdentifier; folderPath?: string; title: string; filename?: string; }[]> { return TPromise.as(void 0); } diff --git a/src/vs/workbench/workbench.main.ts b/src/vs/workbench/workbench.main.ts index 5dfb796f542..59e8a78b8ab 100644 --- a/src/vs/workbench/workbench.main.ts +++ b/src/vs/workbench/workbench.main.ts @@ -86,7 +86,6 @@ import 'vs/workbench/parts/emmet/electron-browser/emmet.contribution'; import 'vs/workbench/parts/codeEditor/codeEditor.contribution'; import 'vs/workbench/parts/execution/electron-browser/execution.contribution'; -import 'vs/workbench/parts/execution/electron-browser/terminal.contribution'; import 'vs/workbench/parts/snippets/electron-browser/snippets.contribution';