diff --git a/.github/calendar.yml b/.github/calendar.yml index 06ea947f56d..276adfcda36 100644 --- a/.github/calendar.yml +++ b/.github/calendar.yml @@ -10,4 +10,6 @@ '2018-03-15 12:00, US/Pacific': 'release', # 1.21.1 '2018-03-20 12:00, US/Pacific': 'development', '2018-03-26 18:00, US/Pacific': 'endgame', -} \ No newline at end of file + '2018-04-06 18:00, US/Pacific': 'release', # 1.22.1 + '2018-04-11 18:00, US/Pacific': 'development', +} diff --git a/.github/classifier.yml b/.github/classifier.yml index dbdd23ddfb7..a83c95ad2ba 100644 --- a/.github/classifier.yml +++ b/.github/classifier.yml @@ -10,7 +10,7 @@ color-picker: [], css-less-sass: [ aeschli ], debug: { - assignees: [ isidorn ], + assignees: [ weinand ], assignLabel: false }, diff-editor: [], @@ -35,15 +35,15 @@ error-list: [], extensions: [], file-encoding: { - assignees: [ bpasero ], + assignees: [], assignLabel: false }, file-io: { - assignees: [ bpasero ], + assignees: [], assignLabel: false }, file-watcher: { - assignees: [ bpasero ], + assignees: [], assignLabel: false }, file-explorer: { @@ -65,7 +65,7 @@ markdown: [ mjbvz ], merge-conflict: [ chrmarti ], multi-root: { - assignees: [ bpasero ], + assignees: [], assignLabel: false }, perf-profile: [], @@ -86,59 +86,59 @@ themes: [], typescript: [ mjbvz ], workbench: { - assignees: [ bpasero ], + assignees: [], assignLabel: false }, workbench-dnd: { - assignees: [ bpasero ], + assignees: [], assignLabel: false }, workbench-editors: { - assignees: [ bpasero ], + assignees: [], assignLabel: false }, workbench-electron: { - assignees: [ bpasero ], + assignees: [], assignLabel: false }, workbench-feedback: { - assignees: [ bpasero ], + assignees: [], assignLabel: false }, workbench-history: { - assignees: [ bpasero ], + assignees: [], assignLabel: false }, workbench-layout: { - assignees: [ bpasero ], + assignees: [], assignLabel: false }, workbench-menu: { - assignees: [ bpasero ], + assignees: [], assignLabel: false }, workbench-notifications: { - assignees: [ bpasero ], + assignees: [], assignLabel: false }, workbench-state: { - assignees: [ bpasero ], + assignees: [], assignLabel: false }, workbench-status: { - assignees: [ bpasero ], + assignees: [], assignLabel: false }, workbench-tabs: { - assignees: [ bpasero ], + assignees: [], assignLabel: false }, workbench-title: { - assignees: [ bpasero ], + assignees: [], assignLabel: false }, workbench-touchbar: { - assignees: [ bpasero ], + assignees: [], assignLabel: false }, workbench-welcome: [ chrmarti ] diff --git a/.travis.yml b/.travis.yml index cb271784ad9..8547e3081cd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -51,7 +51,6 @@ install: script: - node_modules/.bin/gulp electron --silent - - node_modules/.bin/tsc -p ./src/tsconfig.monaco.json --noEmit - node_modules/.bin/gulp compile --silent --max_old_space_size=4096 - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then ./scripts/test.sh --coverage --reporter dot; else ./scripts/test.sh --reporter dot; fi - ./scripts/test-integration.sh diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 6cb9439d5c7..8ad783bcbe3 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,9 +1,9 @@ { - // See http://go.microsoft.com/fwlink/?LinkId=827846 + // See https://go.microsoft.com/fwlink/?LinkId=827846 // for the documentation about the extensions.json format "recommendations": [ "eg2.tslint", "dbaeumer.vscode-eslint", "msjsdiag.debugger-for-chrome" ] -} \ No newline at end of file +} diff --git a/.vscode/launch.json b/.vscode/launch.json index 5b9bd2fff8b..e55a5f5bd4a 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -162,7 +162,7 @@ { "type": "extensionHost", "request": "launch", - "name": "VS Code Markdown Extension Tests", + "name": "Markdown Extension Tests", "runtimeExecutable": "${execPath}", "args": [ "${workspaceFolder}/extensions/markdown-language-features/test-fixtures", diff --git a/ThirdPartyNotices.txt b/ThirdPartyNotices.txt index b0e4112b770..4c4a9b9d6b6 100644 --- a/ThirdPartyNotices.txt +++ b/ThirdPartyNotices.txt @@ -14,31 +14,31 @@ This project incorporates components from the projects listed below. The origina 7. atom/language-sass version 0.52.0 (https://github.com/atom/language-sass) 8. atom/language-shellscript (https://github.com/atom/language-shellscript) 9. atom/language-xml (https://github.com/atom/language-xml) -10. Benvie/JavaScriptNext.tmLanguage (https://github.com/Microsoft/vscode-JSON.tmLanguage) -11. chjj-marked version 0.3.12 (https://github.com/npmcomponent/chjj-marked) -12. chriskempson/tomorrow-theme (https://github.com/chriskempson/tomorrow-theme) -13. Colorsublime-Themes version 0.1.0 (https://github.com/Colorsublime/Colorsublime-Themes) -14. daaain/Handlebars (https://github.com/daaain/Handlebars) -15. davidrios/jade-tmbundle (https://github.com/davidrios/jade-tmbundle) -16. definitelytyped (https://github.com/DefinitelyTyped/DefinitelyTyped) -17. demyte/language-cshtml (https://github.com/demyte/language-cshtml) -18. dotnet/csharp-tmLanguage version 0.1.0 (https://github.com/dotnet/csharp-tmLanguage) -19. expand-abbreviation version 0.5.8 (https://github.com/emmetio/expand-abbreviation) -20. fadeevab/make.tmbundle (https://github.com/fadeevab/make.tmbundle) -21. freebroccolo/atom-language-swift (https://github.com/freebroccolo/atom-language-swift) -22. HTML 5.1 W3C Working Draft version 08 October 2015 (http://www.w3.org/TR/2015/WD-html51-20151008/) -23. Ikuyadeu/vscode-R (https://github.com/Ikuyadeu/vscode-R) -24. Ionic documentation version 1.2.4 (https://github.com/ionic-team/ionic-site) -25. ionide/ionide-fsgrammar (https://github.com/ionide/ionide-fsgrammar) -26. js-beautify version 1.6.8 (https://github.com/beautify-web/js-beautify) -27. Jxck/assert version 1.0.0 (https://github.com/Jxck/assert) -28. language-docker (https://github.com/moby/moby) -29. language-go version 0.39.0 (https://github.com/atom/language-go) -30. language-less (https://github.com/atom/language-less) -31. language-php (https://github.com/atom/language-php) -32. language-rust version 0.4.9 (https://github.com/zargony/atom-language-rust) -33. MagicStack/MagicPython (https://github.com/MagicStack/MagicPython) -34. Microsoft/TypeScript-TmLanguage version 0.0.1 (https://github.com/Microsoft/TypeScript-TmLanguage) +10. chjj-marked version 0.3.18 (https://github.com/npmcomponent/chjj-marked) +11. chriskempson/tomorrow-theme (https://github.com/chriskempson/tomorrow-theme) +12. Colorsublime-Themes version 0.1.0 (https://github.com/Colorsublime/Colorsublime-Themes) +13. daaain/Handlebars (https://github.com/daaain/Handlebars) +14. davidrios/jade-tmbundle (https://github.com/davidrios/jade-tmbundle) +15. definitelytyped (https://github.com/DefinitelyTyped/DefinitelyTyped) +16. demyte/language-cshtml (https://github.com/demyte/language-cshtml) +17. dotnet/csharp-tmLanguage version 0.1.0 (https://github.com/dotnet/csharp-tmLanguage) +18. expand-abbreviation version 0.5.8 (https://github.com/emmetio/expand-abbreviation) +19. fadeevab/make.tmbundle (https://github.com/fadeevab/make.tmbundle) +20. freebroccolo/atom-language-swift (https://github.com/freebroccolo/atom-language-swift) +21. HTML 5.1 W3C Working Draft version 08 October 2015 (http://www.w3.org/TR/2015/WD-html51-20151008/) +22. Ikuyadeu/vscode-R (https://github.com/Ikuyadeu/vscode-R) +23. Ionic documentation version 1.2.4 (https://github.com/ionic-team/ionic-site) +24. ionide/ionide-fsgrammar (https://github.com/ionide/ionide-fsgrammar) +25. js-beautify version 1.6.8 (https://github.com/beautify-web/js-beautify) +26. Jxck/assert version 1.0.0 (https://github.com/Jxck/assert) +27. language-docker (https://github.com/moby/moby) +28. language-go version 0.39.0 (https://github.com/atom/language-go) +29. language-less (https://github.com/atom/language-less) +30. language-php (https://github.com/atom/language-php) +31. language-rust version 0.4.9 (https://github.com/zargony/atom-language-rust) +32. MagicStack/MagicPython (https://github.com/MagicStack/MagicPython) +33. Microsoft/TypeScript-TmLanguage version 0.0.1 (https://github.com/Microsoft/TypeScript-TmLanguage) +34. Microsoft/vscode-JSON.tmLanguage (https://github.com/Microsoft/vscode-JSON.tmLanguage) 35. Microsoft/vscode-mssql (https://github.com/Microsoft/vscode-mssql) 36. mmims/language-batchfile (https://github.com/mmims/language-batchfile) 37. octicons-code version 3.1.0 (https://octicons.github.com) @@ -446,30 +446,6 @@ suitability for any purpose. ========================================= END OF atom/language-xml NOTICES AND INFORMATION -%% Benvie/JavaScriptNext.tmLanguage NOTICES AND INFORMATION BEGIN HERE -========================================= -vscode-JSON.tmLanguage - -Copyright (c) Microsoft Corporation - -All rights reserved. - -MIT License - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -documentation files (the ""Software""), to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, -and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF -CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -========================================= -END OF Benvie/JavaScriptNext.tmLanguage NOTICES AND INFORMATION - %% chjj-marked NOTICES AND INFORMATION BEGIN HERE ========================================= The MIT License (MIT) @@ -1326,6 +1302,30 @@ THE SOFTWARE. ========================================= END OF Microsoft/TypeScript-TmLanguage NOTICES AND INFORMATION +%% Microsoft/vscode-JSON.tmLanguage NOTICES AND INFORMATION BEGIN HERE +========================================= +vscode-JSON.tmLanguage + +Copyright (c) Microsoft Corporation + +All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +documentation files (the ""Software""), to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF +CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF Microsoft/vscode-JSON.tmLanguage NOTICES AND INFORMATION + %% Microsoft/vscode-mssql NOTICES AND INFORMATION BEGIN HERE ========================================= ------------------------------------------ START OF LICENSE ----------------------------------------- diff --git a/appveyor.yml b/appveyor.yml index 3ece36f7a3e..d9471f2a8f8 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -11,7 +11,6 @@ install: build_script: - yarn - .\node_modules\.bin\gulp electron - - .\node_modules\.bin\tsc -p .\src\tsconfig.monaco.json --noEmit - npm run compile test_script: diff --git a/build/builtInExtensions.json b/build/builtInExtensions.json index 3244b9f5d70..ca2bcf5144a 100644 --- a/build/builtInExtensions.json +++ b/build/builtInExtensions.json @@ -1,12 +1,12 @@ [ { "name": "ms-vscode.node-debug", - "version": "1.22.8", + "version": "1.23.1", "repo": "https://github.com/Microsoft/vscode-node-debug" }, { "name": "ms-vscode.node-debug2", - "version": "1.22.5", + "version": "1.23.0", "repo": "https://github.com/Microsoft/vscode-node-debug2" } -] \ No newline at end of file +] diff --git a/build/gulpfile.editor.js b/build/gulpfile.editor.js index a2366ec1720..659ef8b3008 100644 --- a/build/gulpfile.editor.js +++ b/build/gulpfile.editor.js @@ -93,7 +93,7 @@ gulp.task('clean-minified-editor', util.rimraf('out-editor-min')); gulp.task('minify-editor', ['clean-minified-editor', 'optimize-editor'], common.minifyTask('out-editor')); gulp.task('clean-editor-esm', util.rimraf('out-editor-esm')); -gulp.task('extract-editor-esm', ['clean-editor-esm', 'clean-editor-distro'], function() { +gulp.task('extract-editor-esm', ['clean-editor-esm', 'clean-editor-distro'], function () { standalone.createESMSourcesAndResources({ entryPoints: [ 'vs/editor/editor.main', @@ -107,7 +107,7 @@ gulp.task('extract-editor-esm', ['clean-editor-esm', 'clean-editor-distro'], fun } }); }); -gulp.task('compile-editor-esm', ['extract-editor-esm', 'clean-editor-distro'], function() { +gulp.task('compile-editor-esm', ['extract-editor-esm', 'clean-editor-distro'], function () { const result = cp.spawnSync(`node`, [`../node_modules/.bin/tsc`], { cwd: path.join(__dirname, '../out-editor-esm') }); @@ -235,3 +235,60 @@ function filterStream(testFunc) { this.emit('data', data); }); } + + +//#region monaco type checking + +function createTscCompileTask(watch) { + return () => { + const createReporter = require('./lib/reporter').createReporter; + + return new Promise((resolve, reject) => { + const args = ['./node_modules/.bin/tsc', '-p', './src/tsconfig.monaco.json', '--noEmit']; + if (watch) { + args.push('-w'); + } + const child = cp.spawn(`node`, args, { + cwd: path.join(__dirname, '..'), + // stdio: [null, 'pipe', 'inherit'] + }); + let errors = []; + let reporter = createReporter(); + let report; + let magic = /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g; // https://stackoverflow.com/questions/25245716/remove-all-ansi-colors-styles-from-strings + + child.stdout.on('data', data => { + let str = String(data); + str = str.replace(magic, '').trim(); + if (str.indexOf('Starting compilation') >= 0 || str.indexOf('File change detected') >= 0) { + errors.length = 0; + report = reporter.end(false); + + } else if (str.indexOf('Compilation complete') >= 0) { + report.end(); + + } else if (str) { + let match = /(.*\(\d+,\d+\): )(.*: )(.*)/.exec(str); + if (match) { + // trying to massage the message so that it matches the gulp-tsb error messages + // e.g. src/vs/base/common/strings.ts(663,5): error TS2322: Type '1234' is not assignable to type 'string'. + let fullpath = path.join(root, match[1]); + let message = match[3]; + // @ts-ignore + reporter(fullpath + message); + } else { + // @ts-ignore + reporter(str); + } + } + }); + child.on('exit', resolve); + child.on('error', reject); + }); + }; +} + +gulp.task('monaco-typecheck-watch', createTscCompileTask(true)); +gulp.task('monaco-typecheck', createTscCompileTask(false)); + +//#endregion diff --git a/build/gulpfile.hygiene.js b/build/gulpfile.hygiene.js index 6b4cdd2855f..5ea1b5c51a4 100644 --- a/build/gulpfile.hygiene.js +++ b/build/gulpfile.hygiene.js @@ -49,7 +49,6 @@ const indentationFilter = [ '!src/vs/base/common/marked/marked.js', '!src/vs/base/common/winjs.base.js', '!src/vs/base/node/terminateProcess.sh', - '!src/vs/base/node/ps-win.ps1', '!test/assert.js', // except specific folders @@ -62,6 +61,7 @@ const indentationFilter = [ // except multiple specific files '!**/package.json', '!**/yarn.lock', + '!**/yarn-error.log', // except multiple specific folders '!**/octicons/**', diff --git a/build/gulpfile.vscode.js b/build/gulpfile.vscode.js index 403d4faad4c..67c8fe08858 100644 --- a/build/gulpfile.vscode.js +++ b/build/gulpfile.vscode.js @@ -67,18 +67,19 @@ const vscodeEntryPoints = _.flatten([ const vscodeResources = [ 'out-build/main.js', 'out-build/cli.js', + 'out-build/driver.js', 'out-build/bootstrap.js', 'out-build/bootstrap-amd.js', 'out-build/paths.js', 'out-build/vs/**/*.{svg,png,cur,html}', 'out-build/vs/base/common/performance.js', - 'out-build/vs/base/node/{stdForkStart.js,terminateProcess.sh,ps-win.ps1}', + 'out-build/vs/base/node/{stdForkStart.js,terminateProcess.sh}', 'out-build/vs/base/browser/ui/octiconLabel/octicons/**', 'out-build/vs/workbench/browser/media/*-theme.css', 'out-build/vs/workbench/electron-browser/bootstrap/**', 'out-build/vs/workbench/parts/debug/**/*.json', 'out-build/vs/workbench/parts/execution/**/*.scpt', - 'out-build/vs/workbench/parts/html/electron-browser/webview-pre.js', + 'out-build/vs/workbench/parts/webview/electron-browser/webview-pre.js', 'out-build/vs/**/markdown.css', 'out-build/vs/workbench/parts/tasks/**/*.json', 'out-build/vs/workbench/parts/terminal/electron-browser/terminalProcess.js', @@ -87,6 +88,7 @@ const vscodeResources = [ 'out-build/vs/workbench/services/files/**/*.md', 'out-build/vs/code/electron-browser/sharedProcess/sharedProcess.js', 'out-build/vs/code/electron-browser/issue/issueReporter.js', + 'out-build/vs/code/electron-browser/processExplorer/processExplorer.js', '!**/test/**' ]; @@ -275,7 +277,7 @@ function packageTask(platform, arch, opts) { const packageJsonStream = gulp.src(['package.json'], { base: '.' }) .pipe(json({ name, version })); - const settingsSearchBuildId = getBuildNumber(); + const settingsSearchBuildId = getSettingsSearchBuildId(packageJson); const date = new Date().toISOString(); const productJsonStream = gulp.src(['product.json'], { base: '.' }) .pipe(json({ commit, date, checksums, settingsSearchBuildId })); @@ -481,14 +483,12 @@ gulp.task('upload-vscode-configuration', ['generate-vscode-configuration'], () = } if (!fs.existsSync(allConfigDetailsPath)) { - console.error(`configuration file at ${allConfigDetailsPath} does not exist`); - return; + throw new Error(`configuration file at ${allConfigDetailsPath} does not exist`); } - const settingsSearchBuildId = getBuildNumber(); + const settingsSearchBuildId = getSettingsSearchBuildId(packageJson); if (!settingsSearchBuildId) { - console.error('Failed to compute build number'); - return; + throw new Error('Failed to compute build number'); } return gulp.src(allConfigDetailsPath) @@ -500,76 +500,18 @@ gulp.task('upload-vscode-configuration', ['generate-vscode-configuration'], () = })); }); -function getBuildNumber() { - const previous = getPreviousVersion(packageJson.version); - if (!previous) { - return 0; - } +function getSettingsSearchBuildId(packageJson) { + const previous = util.getPreviousVersion(packageJson.version); try { const out = cp.execSync(`git rev-list ${previous}..HEAD --count`); const count = parseInt(out.toString()); - return versionStringToNumber(packageJson.version) * 1e4 + count; + return util.versionStringToNumber(packageJson.version) * 1e4 + count; } catch (e) { - console.error('Could not determine build number: ' + e.toString()); - return 0; + throw new Error('Could not determine build number: ' + e.toString()); } } -/** - * Given 1.17.2, return 1.17.1 - * 1.18.0 => 1.17.2. - * 2.0.0 => 1.18.0 (or the highest 1.x) - */ -function getPreviousVersion(versionStr) { - function tagExists(tagName) { - try { - cp.execSync(`git rev-parse ${tagName}`, { stdio: 'ignore' }); - return true; - } catch (e) { - return false; - } - } - - function getLastTagFromBase(semverArr, componentToTest) { - const baseVersion = semverArr.join('.'); - if (!tagExists(baseVersion)) { - console.error('Failed to find tag for base version, ' + baseVersion); - return null; - } - - let goodTag; - do { - goodTag = semverArr.join('.'); - semverArr[componentToTest]++; - } while (tagExists(semverArr.join('.'))); - - return goodTag; - } - - const semverArr = versionStr.split('.'); - if (semverArr[2] > 0) { - semverArr[2]--; - return semverArr.join('.'); - } else if (semverArr[1] > 0) { - semverArr[1]--; - return getLastTagFromBase(semverArr, 2); - } else { - semverArr[0]--; - return getLastTagFromBase(semverArr, 1); - } -} - -function versionStringToNumber(versionStr) { - const semverRegex = /(\d+)\.(\d+)\.(\d+)/; - const match = versionStr.match(semverRegex); - if (!match) { - return 0; - } - - return parseInt(match[1], 10) * 1e4 + parseInt(match[2], 10) * 1e2 + parseInt(match[3], 10); -} - // This task is only run for the MacOS build gulp.task('generate-vscode-configuration', () => { return new Promise((resolve, reject) => { @@ -601,8 +543,5 @@ gulp.task('generate-vscode-configuration', () => { clearTimeout(timer); reject(err); }); - }).catch(e => { - // Don't fail the build - console.error(e.toString()); }); }); diff --git a/build/lib/compilation.js b/build/lib/compilation.js index 61c178b392e..998ebb4f379 100644 --- a/build/lib/compilation.js +++ b/build/lib/compilation.js @@ -22,7 +22,7 @@ var rootDir = path.join(__dirname, '../../src'); var options = require('../../src/tsconfig.json').compilerOptions; options.verbose = false; options.sourceMap = true; -if (process.env['VSCODE_NO_SOURCEMAP']) { +if (process.env['VSCODE_NO_SOURCEMAP']) { // To be used by developers in a hurry options.sourceMap = false; } options.rootDir = rootDir; diff --git a/build/lib/i18n.js b/build/lib/i18n.js index 31282b6c5bb..61893ed8b1d 100644 --- a/build/lib/i18n.js +++ b/build/lib/i18n.js @@ -1046,7 +1046,10 @@ function createI18nFile(originalFilePath, messages) { var key = _a[_i]; result[key] = messages[key]; } - var content = JSON.stringify(result, null, '\t').replace(/\r\n/g, '\n'); + var content = JSON.stringify(result, null, '\t'); + if (process.platform === 'win32') { + content = content.replace(/\n/g, '\r\n'); + } return new File({ path: path.join(originalFilePath + '.i18n.json'), contents: Buffer.from(content, 'utf8') @@ -1080,7 +1083,7 @@ function prepareI18nPackFiles(externalExtensions, resultingTranslationPaths, pse extPack = extensionsPacks[resource] = { version: i18nPackVersion, contents: {} }; } var externalId = externalExtensions[resource]; - if (!externalId) { + if (!externalId) { // internal extension: remove 'extensions/extensionId/' segnent var secondSlash = path.indexOf('/', firstSlash + 1); extPack.contents[path.substr(secondSlash + 1)] = file.messages; } diff --git a/build/lib/i18n.resources.json b/build/lib/i18n.resources.json index 9de72cc968d..4cf0bc4cf6a 100644 --- a/build/lib/i18n.resources.json +++ b/build/lib/i18n.resources.json @@ -130,6 +130,10 @@ "name": "vs/workbench/parts/update", "project": "vscode-workbench" }, + { + "name": "vs/workbench/parts/url", + "project": "vscode-workbench" + }, { "name": "vs/workbench/parts/watermark", "project": "vscode-workbench" @@ -209,6 +213,10 @@ { "name": "vs/workbench/services/decorations", "project": "vscode-workbench" + }, + { + "name": "vs/workbench/services/preferences", + "project": "vscode-preferences" } ] -} +} \ No newline at end of file diff --git a/build/lib/i18n.ts b/build/lib/i18n.ts index 7c5899ac953..06b51be8767 100644 --- a/build/lib/i18n.ts +++ b/build/lib/i18n.ts @@ -1171,7 +1171,10 @@ function createI18nFile(originalFilePath: string, messages: any): File { result[key] = messages[key]; } - let content = JSON.stringify(result, null, '\t').replace(/\r\n/g, '\n'); + let content = JSON.stringify(result, null, '\t'); + if (process.platform === 'win32') { + content = content.replace(/\n/g, '\r\n'); + } return new File({ path: path.join(originalFilePath + '.i18n.json'), contents: Buffer.from(content, 'utf8') diff --git a/build/lib/nls.js b/build/lib/nls.js index 7f2730ec891..a63d3699014 100644 --- a/build/lib/nls.js +++ b/build/lib/nls.js @@ -150,13 +150,16 @@ function isImportNode(node) { .filter(function (d) { return d.importClause.namedBindings.kind === ts.SyntaxKind.NamespaceImport; }) .map(function (d) { return d.importClause.namedBindings.name; }) .concat(importEqualsDeclarations.map(function (d) { return d.name; })) + // find read-only references to `nls` .map(function (n) { return service.getReferencesAtPosition(filename, n.pos + 1); }) .flatten() .filter(function (r) { return !r.isWriteAccess; }) + // find the deepest call expressions AST nodes that contain those references .map(function (r) { return collect(sourceFile, function (n) { return isCallExpressionWithinTextSpanCollectStep(r.textSpan, n); }); }) .map(function (a) { return lazy(a).last(); }) .filter(function (n) { return !!n; }) .map(function (n) { return n; }) + // only `localize` calls .filter(function (n) { return n.expression.kind === ts.SyntaxKind.PropertyAccessExpression && n.expression.name.getText() === 'localize'; }); // `localize` named imports var allLocalizeImportDeclarations = importDeclarations diff --git a/build/lib/reporter.js b/build/lib/reporter.js index 93fd65351df..7dc3f50e038 100644 --- a/build/lib/reporter.js +++ b/build/lib/reporter.js @@ -34,7 +34,13 @@ catch (err) { } function log() { var errors = _.flatten(allErrors); - errors.map(function (err) { return util.log(util.colors.red('Error') + ": " + err); }); + var seen = new Set(); + errors.map(function (err) { + if (!seen.has(err)) { + seen.add(err); + util.log(util.colors.red('Error') + ": " + err); + } + }); var regex = /^([^(]+)\((\d+),(\d+)\): (.*)$/; var messages = errors .map(function (err) { return regex.exec(err); }) @@ -80,4 +86,3 @@ function createReporter() { return ReportFunc; } exports.createReporter = createReporter; -; diff --git a/build/lib/reporter.ts b/build/lib/reporter.ts index e072a60bbfd..e4be8549ddb 100644 --- a/build/lib/reporter.ts +++ b/build/lib/reporter.ts @@ -11,7 +11,7 @@ import * as util from 'gulp-util'; import * as fs from 'fs'; import * as path from 'path'; -const allErrors: Error[][] = []; +const allErrors: string[][] = []; let startTime: number = null; let count = 0; @@ -42,7 +42,14 @@ try { function log(): void { const errors = _.flatten(allErrors); - errors.map(err => util.log(`${util.colors.red('Error')}: ${err}`)); + const seen = new Set(); + + errors.map(err => { + if (!seen.has(err)) { + seen.add(err); + util.log(`${util.colors.red('Error')}: ${err}`); + } + }); const regex = /^([^(]+)\((\d+),(\d+)\): (.*)$/; const messages = errors @@ -61,17 +68,17 @@ function log(): void { } export interface IReporter { - (err: Error): void; + (err: string): void; hasErrors(): boolean; end(emitError: boolean): NodeJS.ReadWriteStream; } export function createReporter(): IReporter { - const errors: Error[] = []; + const errors: string[] = []; allErrors.push(errors); class ReportFunc { - constructor(err: Error) { + constructor(err: string) { errors.push(err); } @@ -97,4 +104,4 @@ export function createReporter(): IReporter { } return ReportFunc; -}; +} diff --git a/build/lib/test/util.test.js b/build/lib/test/util.test.js new file mode 100644 index 00000000000..ef0616173b6 --- /dev/null +++ b/build/lib/test/util.test.js @@ -0,0 +1,56 @@ +"use strict"; +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +Object.defineProperty(exports, "__esModule", { value: true }); +var assert = require("assert"); +var util = require("../util"); +function getMockTagExists(tags) { + return function (tag) { return tags.indexOf(tag) >= 0; }; +} +suite('util tests', function () { + test('getPreviousVersion - patch', function () { + assert.equal(util.getPreviousVersion('1.2.3', getMockTagExists(['1.2.2', '1.2.1', '1.2.0', '1.1.0'])), '1.2.2'); + }); + test('getPreviousVersion - patch invalid', function () { + try { + util.getPreviousVersion('1.2.2', getMockTagExists(['1.2.0', '1.1.0'])); + } + catch (e) { + // expected + return; + } + throw new Error('Expected an exception'); + }); + test('getPreviousVersion - minor', function () { + assert.equal(util.getPreviousVersion('1.2.0', getMockTagExists(['1.1.0', '1.1.1', '1.1.2', '1.1.3'])), '1.1.3'); + assert.equal(util.getPreviousVersion('1.2.0', getMockTagExists(['1.1.0', '1.0.0'])), '1.1.0'); + }); + test('getPreviousVersion - minor gap', function () { + assert.equal(util.getPreviousVersion('1.2.0', getMockTagExists(['1.1.0', '1.1.1', '1.1.3'])), '1.1.1'); + }); + test('getPreviousVersion - minor invalid', function () { + try { + util.getPreviousVersion('1.2.0', getMockTagExists(['1.0.0'])); + } + catch (e) { + // expected + return; + } + throw new Error('Expected an exception'); + }); + test('getPreviousVersion - major', function () { + assert.equal(util.getPreviousVersion('2.0.0', getMockTagExists(['1.0.0', '1.1.0', '1.2.0', '1.2.1', '1.2.2'])), '1.2.2'); + }); + test('getPreviousVersion - major invalid', function () { + try { + util.getPreviousVersion('3.0.0', getMockTagExists(['1.0.0'])); + } + catch (e) { + // expected + return; + } + throw new Error('Expected an exception'); + }); +}); diff --git a/build/lib/test/util.test.ts b/build/lib/test/util.test.ts new file mode 100644 index 00000000000..928e730f06c --- /dev/null +++ b/build/lib/test/util.test.ts @@ -0,0 +1,79 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert = require('assert'); +import util = require('../util'); + +function getMockTagExists(tags: string[]) { + return (tag: string) => tags.indexOf(tag) >= 0; +} + +suite('util tests', () => { + test('getPreviousVersion - patch', () => { + assert.equal( + util.getPreviousVersion('1.2.3', getMockTagExists(['1.2.2', '1.2.1', '1.2.0', '1.1.0'])), + '1.2.2' + ); + }); + + test('getPreviousVersion - patch invalid', () => { + try { + util.getPreviousVersion('1.2.2', getMockTagExists(['1.2.0', '1.1.0'])); + } catch (e) { + // expected + return; + } + + throw new Error('Expected an exception'); + }); + + test('getPreviousVersion - minor', () => { + assert.equal( + util.getPreviousVersion('1.2.0', getMockTagExists(['1.1.0', '1.1.1', '1.1.2', '1.1.3'])), + '1.1.3' + ); + + assert.equal( + util.getPreviousVersion('1.2.0', getMockTagExists(['1.1.0', '1.0.0'])), + '1.1.0' + ); + }); + + test('getPreviousVersion - minor gap', () => { + assert.equal( + util.getPreviousVersion('1.2.0', getMockTagExists(['1.1.0', '1.1.1', '1.1.3'])), + '1.1.1' + ); + }); + + test('getPreviousVersion - minor invalid', () => { + try { + util.getPreviousVersion('1.2.0', getMockTagExists(['1.0.0'])); + } catch (e) { + // expected + return; + } + + throw new Error('Expected an exception'); + }); + + test('getPreviousVersion - major', () => { + assert.equal( + util.getPreviousVersion('2.0.0', getMockTagExists(['1.0.0', '1.1.0', '1.2.0', '1.2.1', '1.2.2'])), + '1.2.2' + ); + }); + + test('getPreviousVersion - major invalid', () => { + try { + util.getPreviousVersion('3.0.0', getMockTagExists(['1.0.0'])); + } catch (e) { + // expected + return; + } + + throw new Error('Expected an exception'); + }); +}); diff --git a/build/lib/tslint/translationRemindRule.js b/build/lib/tslint/translationRemindRule.js index 1409dc54951..ec56aff1535 100644 --- a/build/lib/tslint/translationRemindRule.js +++ b/build/lib/tslint/translationRemindRule.js @@ -71,7 +71,7 @@ var TranslationRemindRuleWalker = /** @class */ (function (_super) { } }); if (!resourceDefined) { - this.addFailureAtNode(node, "Please add '" + resource + "' to ./builds/lib/i18n.resources.json file to use translations here."); + this.addFailureAtNode(node, "Please add '" + resource + "' to ./build/lib/i18n.resources.json file to use translations here."); } }; TranslationRemindRuleWalker.NLS_MODULE = 'vs/nls'; diff --git a/build/lib/tslint/translationRemindRule.ts b/build/lib/tslint/translationRemindRule.ts index 6bc2a191619..2c5adcc4c49 100644 --- a/build/lib/tslint/translationRemindRule.ts +++ b/build/lib/tslint/translationRemindRule.ts @@ -67,7 +67,7 @@ class TranslationRemindRuleWalker extends Lint.RuleWalker { }); if (!resourceDefined) { - this.addFailureAtNode(node, `Please add '${resource}' to ./builds/lib/i18n.resources.json file to use translations here.`); + this.addFailureAtNode(node, `Please add '${resource}' to ./build/lib/i18n.resources.json file to use translations here.`); } } } diff --git a/build/lib/util.js b/build/lib/util.js index 245ee13ae82..1a2d40327cd 100644 --- a/build/lib/util.js +++ b/build/lib/util.js @@ -14,6 +14,7 @@ var fs = require("fs"); var _rimraf = require("rimraf"); var git = require("./git"); var VinylFile = require("vinyl"); +var cp = require("child_process"); var NoCancellationToken = { isCancellationRequested: function () { return false; } }; function incremental(streamProvider, initial, supportsCancellation) { var input = es.through(); @@ -210,3 +211,68 @@ function filter(fn) { return result; } exports.filter = filter; +function tagExists(tagName) { + try { + cp.execSync("git rev-parse " + tagName, { stdio: 'ignore' }); + return true; + } + catch (e) { + return false; + } +} +/** + * Returns the version previous to the given version. Throws if a git tag for that version doesn't exist. + * Given 1.17.2, return 1.17.1 + * 1.18.0 => 1.17.2. (or the highest 1.17.x) + * 2.0.0 => 1.18.0 (or the highest 1.x) + */ +function getPreviousVersion(versionStr, _tagExists) { + if (_tagExists === void 0) { _tagExists = tagExists; } + function getLatestTagFromBase(semverArr, componentToTest) { + var baseVersion = semverArr.join('.'); + if (!_tagExists(baseVersion)) { + throw new Error('Failed to find git tag for base version, ' + baseVersion); + } + var goodTag; + do { + goodTag = semverArr.join('.'); + semverArr[componentToTest]++; + } while (_tagExists(semverArr.join('.'))); + return goodTag; + } + var semverArr = versionStringToNumberArray(versionStr); + if (semverArr[2] > 0) { + semverArr[2]--; + var previous = semverArr.join('.'); + if (!_tagExists(previous)) { + throw new Error('Failed to find git tag for previous version, ' + previous); + } + return previous; + } + else if (semverArr[1] > 0) { + semverArr[1]--; + return getLatestTagFromBase(semverArr, 2); + } + else { + semverArr[0]--; + // Find 1.x.0 for latest x + var latestMinorVersion = getLatestTagFromBase(semverArr, 1); + // Find 1.x.y for latest y + return getLatestTagFromBase(versionStringToNumberArray(latestMinorVersion), 2); + } +} +exports.getPreviousVersion = getPreviousVersion; +function versionStringToNumberArray(versionStr) { + return versionStr + .split('.') + .map(function (s) { return parseInt(s); }); +} +function versionStringToNumber(versionStr) { + var semverRegex = /(\d+)\.(\d+)\.(\d+)/; + var match = versionStr.match(semverRegex); + if (!match) { + throw new Error('Version string is not properly formatted: ' + versionStr); + } + return parseInt(match[1], 10) * 1e4 + parseInt(match[2], 10) * 1e2 + parseInt(match[3], 10); +} +exports.versionStringToNumber = versionStringToNumber; diff --git a/build/lib/util.ts b/build/lib/util.ts index bae298f5f05..9dcbbe72484 100644 --- a/build/lib/util.ts +++ b/build/lib/util.ts @@ -17,6 +17,7 @@ import * as git from './git'; import * as VinylFile from 'vinyl'; import { ThroughStream } from 'through'; import * as sm from 'source-map'; +import * as cp from 'child_process'; export interface ICancellationToken { isCancellationRequested(): boolean; @@ -268,4 +269,74 @@ export function filter(fn: (data: any) => boolean): FilterStream { result.restore = es.through(); return result; -} \ No newline at end of file +} + +function tagExists(tagName: string): boolean { + try { + cp.execSync(`git rev-parse ${tagName}`, { stdio: 'ignore' }); + return true; + } catch (e) { + return false; + } +} + +/** + * Returns the version previous to the given version. Throws if a git tag for that version doesn't exist. + * Given 1.17.2, return 1.17.1 + * 1.18.0 => 1.17.2. (or the highest 1.17.x) + * 2.0.0 => 1.18.0 (or the highest 1.x) + */ +export function getPreviousVersion(versionStr: string, _tagExists = tagExists) { + function getLatestTagFromBase(semverArr: number[], componentToTest: number): string { + const baseVersion = semverArr.join('.'); + if (!_tagExists(baseVersion)) { + throw new Error('Failed to find git tag for base version, ' + baseVersion); + } + + let goodTag; + do { + goodTag = semverArr.join('.'); + semverArr[componentToTest]++; + } while (_tagExists(semverArr.join('.'))); + + return goodTag; + } + + const semverArr = versionStringToNumberArray(versionStr); + if (semverArr[2] > 0) { + semverArr[2]--; + const previous = semverArr.join('.'); + if (!_tagExists(previous)) { + throw new Error('Failed to find git tag for previous version, ' + previous); + } + + return previous; + } else if (semverArr[1] > 0) { + semverArr[1]--; + return getLatestTagFromBase(semverArr, 2); + } else { + semverArr[0]--; + + // Find 1.x.0 for latest x + const latestMinorVersion = getLatestTagFromBase(semverArr, 1); + + // Find 1.x.y for latest y + return getLatestTagFromBase(versionStringToNumberArray(latestMinorVersion), 2); + } +} + +function versionStringToNumberArray(versionStr: string): number[] { + return versionStr + .split('.') + .map(s => parseInt(s)); +} + +export function versionStringToNumber(versionStr: string) { + const semverRegex = /(\d+)\.(\d+)\.(\d+)/; + const match = versionStr.match(semverRegex); + if (!match) { + throw new Error('Version string is not properly formatted: ' + versionStr); + } + + return parseInt(match[1], 10) * 1e4 + parseInt(match[2], 10) * 1e2 + parseInt(match[3], 10); +} diff --git a/build/monaco/package.json b/build/monaco/package.json index 958b4ee71c3..256ca1ff534 100644 --- a/build/monaco/package.json +++ b/build/monaco/package.json @@ -1,16 +1,17 @@ { "name": "monaco-editor-core", "private": true, - "version": "0.11.7", + "version": "0.12.0", "description": "A browser based code editor", "author": "Microsoft Corporation", "license": "MIT", + "typings": "./esm/vs/editor/editor.api.d.ts", "module": "./esm/vs/editor/editor.main.js", "repository": { "type": "git", "url": "https://github.com/Microsoft/vscode" }, - "bugs": { + "bugs": { "url": "https://github.com/Microsoft/vscode/issues" } } diff --git a/build/package.json b/build/package.json index 2d8dc9089f0..ade3af750cb 100644 --- a/build/package.json +++ b/build/package.json @@ -9,12 +9,15 @@ "@types/mime": "0.0.29", "@types/node": "8.0.33", "@types/xml2js": "0.0.33", + "@types/request": "^2.47.0", "azure-storage": "^2.1.0", "documentdb": "1.13.0", "mime": "^1.3.4", "minimist": "^1.2.0", - "typescript": "2.7.2", - "xml2js": "^0.4.17" + "typescript": "2.8.1", + "xml2js": "^0.4.17", + "github-releases": "^0.4.1", + "request": "^2.85.0" }, "scripts": { "compile": "tsc -p tsconfig.build.json", diff --git a/build/tfs/common/symbols.ts b/build/tfs/common/symbols.ts new file mode 100644 index 00000000000..eac0d610b73 --- /dev/null +++ b/build/tfs/common/symbols.ts @@ -0,0 +1,219 @@ +/*--------------------------------------------------------------------------------------------- + * 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 request from 'request'; +import { createReadStream, createWriteStream, unlink, mkdir } from 'fs'; +import * as github from 'github-releases'; +import { join } from 'path'; +import { tmpdir } from 'os'; +import { promisify } from 'util'; + +const BASE_URL = 'https://rink.hockeyapp.net/api/2/'; +const HOCKEY_APP_TOKEN_HEADER = 'X-HockeyAppToken'; + +export interface IVersions { + app_versions: IVersion[]; +} + +export interface IVersion { + id: number; + version: string; +} + +export interface IApplicationAccessor { + accessToken: string; + appId: string; +} + +export interface IVersionAccessor extends IApplicationAccessor { + id: string; +} + +enum Platform { + WIN_32 = 'win32-ia32', + WIN_64 = 'win32-x64', + LINUX_32 = 'linux-ia32', + LINUX_64 = 'linux-x64', + MAC_OS = 'darwin-x64' +} + +function symbolsZipName(platform: Platform, electronVersion: string, insiders: boolean): string { + return `${insiders ? 'insiders' : 'stable'}-symbols-v${electronVersion}-${platform}.zip`; +} + +const SEED = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; +async function tmpFile(name: string): Promise { + let res = ''; + for (let i = 0; i < 8; i++) { + res += SEED.charAt(Math.floor(Math.random() * SEED.length)); + } + + const tmpParent = join(tmpdir(), res); + + await promisify(mkdir)(tmpParent); + + return join(tmpParent, name); +} + +function getVersions(accessor: IApplicationAccessor): Promise { + return asyncRequest({ + url: `${BASE_URL}/apps/${accessor.appId}/app_versions`, + method: 'GET', + headers: { + [HOCKEY_APP_TOKEN_HEADER]: accessor.accessToken + } + }); +} + +function createVersion(accessor: IApplicationAccessor, version: string): Promise { + return asyncRequest({ + url: `${BASE_URL}/apps/${accessor.appId}/app_versions/new`, + method: 'POST', + headers: { + [HOCKEY_APP_TOKEN_HEADER]: accessor.accessToken + }, + formData: { + bundle_version: version + } + }); +} + +function updateVersion(accessor: IVersionAccessor, symbolsPath: string) { + return asyncRequest({ + url: `${BASE_URL}/apps/${accessor.appId}/app_versions/${accessor.id}`, + method: 'PUT', + headers: { + [HOCKEY_APP_TOKEN_HEADER]: accessor.accessToken + }, + formData: { + dsym: createReadStream(symbolsPath) + } + }); +} + +function asyncRequest(options: request.UrlOptions & request.CoreOptions): Promise { + return new Promise((resolve, reject) => { + request(options, (error, response, body) => { + if (error) { + reject(error); + } else { + resolve(JSON.parse(body)); + } + }); + }); +} + +function downloadAsset(repository, assetName: string, targetPath: string, electronVersion: string) { + return new Promise((resolve, reject) => { + repository.getReleases({ tag_name: `v${electronVersion}` }, (err, releases) => { + if (err) { + reject(err); + } else { + const asset = releases[0].assets.filter(asset => asset.name === assetName)[0]; + if (!asset) { + reject(new Error(`Asset with name ${assetName} not found`)); + } else { + repository.downloadAsset(asset, (err, reader) => { + if (err) { + reject(err); + } else { + const writer = createWriteStream(targetPath); + writer.on('error', reject); + writer.on('close', resolve); + reader.on('error', reject); + + reader.pipe(writer); + } + }); + } + } + }); + }); +} + +interface IOptions { + repository: string; + platform: Platform; + versions: { code: string; insiders: boolean; electron: string; }; + access: { hockeyAppToken: string; hockeyAppId: string; githubToken: string }; +} + +async function ensureVersionAndSymbols(options: IOptions) { + + // Check version does not exist + console.log(`HockeyApp: checking for existing version ${options.versions.code} (${options.platform})`); + const versions = await getVersions({ accessToken: options.access.hockeyAppToken, appId: options.access.hockeyAppId }); + if (versions.app_versions.some(v => v.version === options.versions.code)) { + console.log(`HockeyApp: Returning without uploading symbols because version ${options.versions.code} (${options.platform}) was already found`); + return; + } + + // Download symbols for platform and electron version + const symbolsName = symbolsZipName(options.platform, options.versions.electron, options.versions.insiders); + const symbolsPath = await tmpFile('symbols.zip'); + console.log(`HockeyApp: downloading symbols ${symbolsName} for electron ${options.versions.electron} (${options.platform}) into ${symbolsPath}`); + await downloadAsset(new github({ repo: options.repository, token: options.access.githubToken }), symbolsName, symbolsPath, options.versions.electron); + + // Create version + console.log(`HockeyApp: creating new version ${options.versions.code} (${options.platform})`); + const version = await createVersion({ accessToken: options.access.hockeyAppToken, appId: options.access.hockeyAppId }, options.versions.code); + + // Upload symbols + console.log(`HockeyApp: uploading symbols for version ${options.versions.code} (${options.platform})`); + await updateVersion({ id: String(version.id), accessToken: options.access.hockeyAppToken, appId: options.access.hockeyAppId }, symbolsPath); + + // Cleanup + await promisify(unlink)(symbolsPath); +} + +// Environment +const pakage = require('../../../package.json'); +const product = require('../../../product.json'); +const repository = product.electronRepository; +const electronVersion = require('../../lib/electron').getElectronVersion(); +const insiders = product.quality !== 'stable'; +let codeVersion = pakage.version; +if (insiders) { + codeVersion = `${codeVersion}-insider`; +} +const githubToken = process.argv[2]; +const hockeyAppToken = process.argv[3]; +const is64 = process.argv[4] === 'x64'; +const hockeyAppId = process.argv[5]; + +let platform: Platform; +if (process.platform === 'darwin') { + platform = Platform.MAC_OS; +} else if (process.platform === 'win32') { + platform = is64 ? Platform.WIN_64 : Platform.WIN_32; +} else { + platform = is64 ? Platform.LINUX_64 : Platform.LINUX_32; +} + +// Create version and upload symbols in HockeyApp +if (repository && codeVersion && electronVersion && (product.quality === 'stable' || product.quality === 'insider')) { + ensureVersionAndSymbols({ + repository, + platform, + versions: { + code: codeVersion, + insiders, + electron: electronVersion + }, + access: { + githubToken, + hockeyAppToken, + hockeyAppId + } + }).then(() => { + console.log('HockeyApp: done'); + }).catch(error => { + console.error(`HockeyApp: error (${error})`); + }); +} else { + console.log(`HockeyApp: skipping due to unexpected context (repository: ${repository}, codeVersion: ${codeVersion}, electronVersion: ${electronVersion}, quality: ${product.quality})`); +} \ No newline at end of file diff --git a/build/tfs/continuous-build.yml b/build/tfs/continuous-build.yml index b5333d7de8d..107c7ff9cdd 100644 --- a/build/tfs/continuous-build.yml +++ b/build/tfs/continuous-build.yml @@ -9,26 +9,34 @@ phases: inputs: versionSpec: "1.3.2" - powershell: | + $ErrorActionPreference = "Stop" yarn .\node_modules\.bin\gulp electron npm run gulp -- hygiene .\node_modules\.bin\tsc -p .\src\tsconfig.monaco.json --noEmit npm run compile + node build/lib/builtInExtensions.js name: build - powershell: | + $ErrorActionPreference = "Stop" .\scripts\test.bat --tfs .\scripts\test-integration.bat + yarn smoketest --screenshots "$(Build.ArtifactStagingDirectory)\artifacts" --log "$(Build.ArtifactStagingDirectory)\artifacts\smoketest.log" name: test - - task: PublishTestResults@2 + - task: PublishBuildArtifacts@1 inputs: - testResultsFiles: '.build\tests\unit-test-results.xml' + PathtoPublish: '$(Build.ArtifactStagingDirectory)/artifacts' + ArtifactName: build-artifacts-win32 + publishLocation: Container + condition: succeededOrFailed() - phase: Linux queue: Hosted Linux Preview steps: - script: | + set -e apt-get update - apt-get install -y libxkbfile-dev pkg-config libsecret-1-dev libxss1 libgconf-2-4 dbus xvfb + apt-get install -y libxkbfile-dev pkg-config libsecret-1-dev libxss1 libgconf-2-4 dbus xvfb libgtk-3-0 cp build/tfs/linux/x64/xvfb.init /etc/init.d/xvfb chmod +x /etc/init.d/xvfb update-rc.d xvfb defaults @@ -42,20 +50,21 @@ phases: inputs: versionSpec: "1.3.2" - script: | + set -e yarn npm run gulp -- electron-x64 - script: | + set -e npm run gulp -- hygiene ./node_modules/.bin/tsc -p ./src/tsconfig.monaco.json --noEmit npm run compile + node build/lib/builtInExtensions.js name: build - script: | + set -e DISPLAY=:10 ./scripts/test.sh --tfs # DISPLAY=:10 ./scripts/test-integration.sh name: test - - task: PublishTestResults@2 - inputs: - testResultsFiles: '.build/tests/unit-test-results.xml' - phase: macOS queue: Hosted macOS Preview @@ -67,17 +76,25 @@ phases: inputs: versionSpec: "1.3.2" - script: | + set -e yarn npm run gulp -- electron-x64 - script: | + set -e npm run gulp -- hygiene ./node_modules/.bin/tsc -p ./src/tsconfig.monaco.json --noEmit npm run compile + node build/lib/builtInExtensions.js name: build - script: | + set -e ./scripts/test.sh --tfs ./scripts/test-integration.sh + yarn smoketest --screenshots "$(Build.ArtifactStagingDirectory)/artifacts" --log "$(Build.ArtifactStagingDirectory)/artifacts/smoketest.log" name: test - - task: PublishTestResults@2 + - task: PublishBuildArtifacts@1 inputs: - testResultsFiles: '.build/tests/unit-test-results.xml' + PathtoPublish: '$(Build.ArtifactStagingDirectory)/artifacts' + ArtifactName: build-artifacts-darwin + publishLocation: Container + condition: succeededOrFailed() \ No newline at end of file diff --git a/build/tfs/product-build.yml b/build/tfs/product-build.yml index 5becec89bca..2a0f41bb644 100644 --- a/build/tfs/product-build.yml +++ b/build/tfs/product-build.yml @@ -29,6 +29,7 @@ phases: $env:VSCODE_MIXIN_PASSWORD="$(VSCODE_MIXIN_PASSWORD)" npm run gulp -- mixin node build/tfs/common/installDistro.js + node build/lib/builtInExtensions.js - powershell: | $env:VSCODE_MIXIN_PASSWORD="$(VSCODE_MIXIN_PASSWORD)" @@ -39,6 +40,7 @@ phases: - powershell: | npm run gulp -- "electron-$(VSCODE_ARCH)" .\scripts\test.bat --build --tfs + # yarn smoketest -- --build "$(agent.builddirectory)\VSCode-win32-$(VSCODE_ARCH)" name: test - task: SFP.build-tasks.custom-build-task-1.EsrpCodeSigning@1 @@ -55,11 +57,11 @@ phases: "parameters": [ { "parameterName": "OpusName", - "parameterValue": "Microsoft" + "parameterValue": "VS Code" }, { "parameterName": "OpusInfo", - "parameterValue": "http://www.microsoft.com" + "parameterValue": "https://code.visualstudio.com/" }, { "parameterName": "PageHash", @@ -79,11 +81,11 @@ phases: "parameters": [ { "parameterName": "OpusName", - "parameterValue": "Microsoft" + "parameterValue": "VS Code" }, { "parameterName": "OpusInfo", - "parameterValue": "http://www.microsoft.com" + "parameterValue": "https://code.visualstudio.com/" }, { "parameterName": "Append", @@ -137,11 +139,11 @@ phases: "parameters": [ { "parameterName": "OpusName", - "parameterValue": "Microsoft" + "parameterValue": "VS Code" }, { "parameterName": "OpusInfo", - "parameterValue": "http://www.microsoft.com" + "parameterValue": "https://code.visualstudio.com/" }, { "parameterName": "PageHash", @@ -161,11 +163,11 @@ phases: "parameters": [ { "parameterName": "OpusName", - "parameterValue": "Microsoft" + "parameterValue": "VS Code" }, { "parameterName": "OpusInfo", - "parameterValue": "http://www.microsoft.com" + "parameterValue": "https://code.visualstudio.com/" }, { "parameterName": "Append", @@ -222,80 +224,17 @@ phases: node build/tfs/common/publish.js $Quality "$global:assetPlatform-archive" archive "VSCode-win32-$(VSCODE_ARCH)-$Version.zip" $Version true $Zip node build/tfs/common/publish.js $Quality "$global:assetPlatform" setup "VSCodeSetup-$(VSCODE_ARCH)-$Version.exe" $Version true $Exe + # publish hockeyapp symbols + $hockeyAppId = if ("$(VSCODE_ARCH)" -eq "ia32") { "$(VSCODE_HOCKEYAPP_ID_WIN32)" } else { "$(VSCODE_HOCKEYAPP_ID_WIN64)" } + node build/tfs/common/symbols.js "$(VSCODE_MIXIN_PASSWORD)" "$(VSCODE_HOCKEYAPP_TOKEN)" "$(VSCODE_ARCH)" $hockeyAppId + - phase: Linux condition: eq(variables['VSCODE_BUILD_LINUX'], 'true') - queue: - name: Hosted Linux Preview - parallel: 2 - matrix: - x64: - VSCODE_ARCH: x64 - ia32: - VSCODE_ARCH: ia32 + queue: linux-x64 + variables: + VSCODE_ARCH: x64 steps: - - script: | - # dependencies - dpkg --add-architecture i386 - apt-get update - - DEPS=" \ - gcc-multilib g++-multilib \ - pkg-config \ - dbus \ - xvfb \ - fakeroot \ - bc \ - bsdmainutils \ - rpm \ - " - - if [[ "$(VSCODE_ARCH)" == "x64" ]]; then - DEPS="$DEPS \ - dpkg-dev \ - libgconf-2-4 \ - libnss3 \ - libasound2 \ - libxtst6 \ - libx11-dev \ - libxkbfile-dev \ - libxss1 \ - libx11-xcb-dev \ - libsecret-1-dev \ - " - else - DEPS="$DEPS \ - dpkg-dev:i386 \ - libgconf-2-4:i386 \ - libnss3:i386 \ - libasound2:i386 \ - libxtst6:i386 \ - libnotify4:i386 \ - libx11-dev:i386 \ - libxkbfile-dev:i386 \ - libxss1:i386 \ - libx11-xcb-dev:i386 \ - libgl1-mesa-glx:i386 libgl1-mesa-dri:i386 \ - libgirepository-1.0-1:i386 \ - gir1.2-glib-2.0:i386 \ - gir1.2-secret-1:i386 \ - libsecret-1-dev:i386 \ - libgtk2.0-0:i386 \ - " - fi - - apt-get install -y $DEPS - - # setup xvfb - cp build/tfs/linux/$(VSCODE_ARCH)/xvfb.init /etc/init.d/xvfb - chmod +x /etc/init.d/xvfb - update-rc.d xvfb defaults - service xvfb start - - # setup dbus - ln -sf /bin/dbus-daemon /usr/bin/dbus-daemon - service dbus start - - task: NodeTool@0 inputs: versionSpec: "8.9.1" @@ -316,6 +255,7 @@ phases: npm run monaco-compile-check VSCODE_MIXIN_PASSWORD="$(VSCODE_MIXIN_PASSWORD)" npm run gulp -- mixin node build/tfs/common/installDistro.js + node build/lib/builtInExtensions.js - script: | VSCODE_MIXIN_PASSWORD="$(VSCODE_MIXIN_PASSWORD)" npm run gulp -- vscode-linux-$(VSCODE_ARCH)-min @@ -324,6 +264,7 @@ phases: - script: | npm run gulp -- "electron-$(VSCODE_ARCH)" DISPLAY=:10 ./scripts/test.sh --build --tfs + # yarn smoketest -- --build "$(agent.builddirectory)/VSCode-linux-$(VSCODE_ARCH)" name: test - script: | @@ -336,6 +277,61 @@ phases: MOONCAKE_STORAGE_ACCESS_KEY="$(MOONCAKE_STORAGE_ACCESS_KEY)" \ ./build/tfs/linux/release2.sh "$(VSCODE_ARCH)" "$(LINUX_REPO_PASSWORD)" + # publish hockeyapp symbols + node build/tfs/common/symbols.js "$(VSCODE_MIXIN_PASSWORD)" "$(VSCODE_HOCKEYAPP_TOKEN)" "$(VSCODE_ARCH)" "$(VSCODE_HOCKEYAPP_ID_LINUX64)" + +- phase: Linux32 + condition: eq(variables['VSCODE_BUILD_LINUX'], 'true') + queue: linux-ia32 + variables: + VSCODE_ARCH: ia32 + + steps: + - task: NodeTool@0 + inputs: + versionSpec: "8.9.1" + + - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 + inputs: + versionSpec: "1.3.2" + + - script: | + export npm_config_arch="$(VSCODE_ARCH)" + if [[ "$(VSCODE_ARCH)" == "ia32" ]]; then + export PKG_CONFIG_PATH="/usr/lib/i386-linux-gnu/pkgconfig" + fi + + echo "machine monacotools.visualstudio.com password $(VSO_PAT)" > ~/.netrc + yarn + npm run gulp -- hygiene + npm run monaco-compile-check + VSCODE_MIXIN_PASSWORD="$(VSCODE_MIXIN_PASSWORD)" npm run gulp -- mixin + node build/tfs/common/installDistro.js + node build/lib/builtInExtensions.js + + - script: | + VSCODE_MIXIN_PASSWORD="$(VSCODE_MIXIN_PASSWORD)" npm run gulp -- vscode-linux-$(VSCODE_ARCH)-min + name: build + + - script: | + npm run gulp -- "electron-$(VSCODE_ARCH)" + DISPLAY=:10 ./scripts/test.sh --build --tfs + # yarn smoketest -- --build "$(agent.builddirectory)/VSCode-linux-$(VSCODE_ARCH)" + name: test + + - script: | + npm run gulp -- "vscode-linux-$(VSCODE_ARCH)-build-deb" + npm run gulp -- "vscode-linux-$(VSCODE_ARCH)-build-rpm" + #npm run gulp -- "vscode-linux-$(VSCODE_ARCH)-build-snap" + + AZURE_DOCUMENTDB_MASTERKEY="$(AZURE_DOCUMENTDB_MASTERKEY)" \ + AZURE_STORAGE_ACCESS_KEY_2="$(AZURE_STORAGE_ACCESS_KEY_2)" \ + MOONCAKE_STORAGE_ACCESS_KEY="$(MOONCAKE_STORAGE_ACCESS_KEY)" \ + ./build/tfs/linux/release2.sh "$(VSCODE_ARCH)" "$(LINUX_REPO_PASSWORD)" + + # publish hockeyapp symbols + node build/tfs/common/symbols.js "$(VSCODE_MIXIN_PASSWORD)" "$(VSCODE_HOCKEYAPP_TOKEN)" "$(VSCODE_ARCH)" "$(VSCODE_HOCKEYAPP_ID_LINUX32)" + - phase: macOS condition: eq(variables['VSCODE_BUILD_MACOS'], 'true') queue: Hosted macOS Preview @@ -355,6 +351,7 @@ phases: npm run monaco-compile-check VSCODE_MIXIN_PASSWORD="$(VSCODE_MIXIN_PASSWORD)" npm run gulp -- mixin node build/tfs/common/installDistro.js + node build/lib/builtInExtensions.js - script: | VSCODE_MIXIN_PASSWORD="$(VSCODE_MIXIN_PASSWORD)" \ @@ -364,6 +361,8 @@ phases: - script: | ./scripts/test.sh --build --tfs + APP_NAME="`ls $(agent.builddirectory)/VSCode-darwin | head -n 1`" + # yarn smoketest -- --build "$(agent.builddirectory)/VSCode-darwin/$APP_NAME" name: test - script: | @@ -385,6 +384,9 @@ phases: false \ ../VSCode-darwin-unsigned.zip + # publish hockeyapp symbols + node build/tfs/common/symbols.js "$(VSCODE_MIXIN_PASSWORD)" "$(VSCODE_HOCKEYAPP_TOKEN)" "$(VSCODE_ARCH)" "$(VSCODE_HOCKEYAPP_ID_MACOS)" + # enqueue the unsigned build AZURE_DOCUMENTDB_MASTERKEY="$(AZURE_DOCUMENTDB_MASTERKEY)" \ AZURE_STORAGE_ACCESS_KEY_2="$(AZURE_STORAGE_ACCESS_KEY_2)" \ diff --git a/build/win32/OSSREADME.json b/build/win32/OSSREADME.json index cbcef582e54..10f47d06684 100755 --- a/build/win32/OSSREADME.json +++ b/build/win32/OSSREADME.json @@ -1,10 +1,10 @@ [ { - "name": "rust-lang-nursery/lazy-static.rs", - "version": "1.0.0", - "repositoryUrl": "https://github.com/rust-lang-nursery/lazy-static.rs", + "name": "rust-lang/libc", + "version": "0.2.36", + "repositoryUrl": "https://github.com/rust-lang/libc", "licenseDetail": [ - "Copyright (c) 2010 The Rust Project Developers", + "Copyright (c) 2014 The Rust Project Developers", "", "Permission is hereby granted, free of charge, to any", "person obtaining a copy of this software and associated", @@ -32,6 +32,33 @@ ], "isProd": true }, + { + "name": "retep998/winapi-rs", + "version": "0.4.0", + "repositoryUrl": "https://github.com/retep998/winapi-rs", + "licenseDetail": [ + "Copyright (c) 2015 The winapi-rs Developers", + "", + "Permission is hereby granted, free of charge, to any person obtaining a copy", + "of this software and associated documentation files (the \"Software\"), to deal", + "in the Software without restriction, including without limitation the rights", + "to use, copy, modify, merge, publish, distribute, sublicense, and/or sell", + "copies of the Software, and to permit persons to whom the Software is", + "furnished to do so, subject to the following conditions:", + "", + "The above copyright notice and this permission notice shall be included in all", + "copies or substantial portions of the Software.", + "", + "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR", + "IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,", + "FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE", + "AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER", + "LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,", + "OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE", + "SOFTWARE." + ], + "isProd": true + }, { "name": "rust-num/num", "version": "0.1.41", @@ -65,6 +92,35 @@ ], "isProd": true }, + { + "name": "BurntSushi/byteorder", + "version": "1.2.1", + "repositoryUrl": "https://github.com/BurntSushi/byteorder", + "licenseDetail": [ + "The MIT License (MIT)", + "", + "Copyright (c) 2015 Andrew Gallant", + "", + "Permission is hereby granted, free of charge, to any person obtaining a copy", + "of this software and associated documentation files (the \"Software\"), to deal", + "in the Software without restriction, including without limitation the rights", + "to use, copy, modify, merge, publish, distribute, sublicense, and/or sell", + "copies of the Software, and to permit persons to whom the Software is", + "furnished to do so, subject to the following conditions:", + "", + "The above copyright notice and this permission notice shall be included in", + "all copies or substantial portions of the Software.", + "", + "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR", + "IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,", + "FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE", + "AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER", + "LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,", + "OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN", + "THE SOFTWARE." + ], + "isProd": true + }, { "name": "dtolnay/isatty", "version": "0.1.6", @@ -98,6 +154,72 @@ ], "isProd": true }, + { + "name": "rust-lang-nursery/lazy-static.rs", + "version": "1.0.0", + "repositoryUrl": "https://github.com/rust-lang-nursery/lazy-static.rs", + "licenseDetail": [ + "Copyright (c) 2010 The Rust Project Developers", + "", + "Permission is hereby granted, free of charge, to any", + "person obtaining a copy of this software and associated", + "documentation files (the \"Software\"), to deal in the", + "Software without restriction, including without", + "limitation the rights to use, copy, modify, merge,", + "publish, distribute, sublicense, and/or sell copies of", + "the Software, and to permit persons to whom the Software", + "is furnished to do so, subject to the following", + "conditions:", + "", + "The above copyright notice and this permission notice", + "shall be included in all copies or substantial portions", + "of the Software.", + "", + "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF", + "ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED", + "TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A", + "PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT", + "SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY", + "CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION", + "OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR", + "IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER", + "DEALINGS IN THE SOFTWARE." + ], + "isProd": true + }, + { + "name": "rust-num/num-integer", + "version": "0.1.35", + "repositoryUrl": "https://github.com/rust-num/num-integer", + "licenseDetail": [ + "Copyright (c) 2014 The Rust Project Developers", + "", + "Permission is hereby granted, free of charge, to any", + "person obtaining a copy of this software and associated", + "documentation files (the \"Software\"), to deal in the", + "Software without restriction, including without", + "limitation the rights to use, copy, modify, merge,", + "publish, distribute, sublicense, and/or sell copies of", + "the Software, and to permit persons to whom the Software", + "is furnished to do so, subject to the following", + "conditions:", + "", + "The above copyright notice and this permission notice", + "shall be included in all copies or substantial portions", + "of the Software.", + "", + "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF", + "ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED", + "TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A", + "PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT", + "SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY", + "CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION", + "OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR", + "IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER", + "DEALINGS IN THE SOFTWARE." + ], + "isProd": true + }, { "name": "retep998/winapi-rs", "version": "0.2.2", @@ -126,100 +248,9 @@ "isProd": true }, { - "name": "BurntSushi/byteorder", - "version": "1.2.1", - "repositoryUrl": "https://github.com/BurntSushi/byteorder", - "licenseDetail": [ - "The MIT License (MIT)", - "", - "Copyright (c) 2015 Andrew Gallant", - "", - "Permission is hereby granted, free of charge, to any person obtaining a copy", - "of this software and associated documentation files (the \"Software\"), to deal", - "in the Software without restriction, including without limitation the rights", - "to use, copy, modify, merge, publish, distribute, sublicense, and/or sell", - "copies of the Software, and to permit persons to whom the Software is", - "furnished to do so, subject to the following conditions:", - "", - "The above copyright notice and this permission notice shall be included in", - "all copies or substantial portions of the Software.", - "", - "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR", - "IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,", - "FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE", - "AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER", - "LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,", - "OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN", - "THE SOFTWARE." - ], - "isProd": true - }, - { - "name": "rust-num/num-integer", - "version": "0.1.35", - "repositoryUrl": "https://github.com/rust-num/num-integer", - "licenseDetail": [ - "Copyright (c) 2014 The Rust Project Developers", - "", - "Permission is hereby granted, free of charge, to any", - "person obtaining a copy of this software and associated", - "documentation files (the \"Software\"), to deal in the", - "Software without restriction, including without", - "limitation the rights to use, copy, modify, merge,", - "publish, distribute, sublicense, and/or sell copies of", - "the Software, and to permit persons to whom the Software", - "is furnished to do so, subject to the following", - "conditions:", - "", - "The above copyright notice and this permission notice", - "shall be included in all copies or substantial portions", - "of the Software.", - "", - "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF", - "ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED", - "TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A", - "PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT", - "SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY", - "CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION", - "OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR", - "IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER", - "DEALINGS IN THE SOFTWARE." - ], - "isProd": true - }, - { - "name": "mrhooray/crc-rs", - "version": "1.7.0", - "repositoryUrl": "https://github.com/mrhooray/crc-rs.git", - "licenseDetail": [ - "MIT License", - "", - "Copyright (c) 2017 crc-rs Developers", - "", - "Permission is hereby granted, free of charge, to any person obtaining a copy", - "of this software and associated documentation files (the \"Software\"), to deal", - "in the Software without restriction, including without limitation the rights", - "to use, copy, modify, merge, publish, distribute, sublicense, and/or sell", - "copies of the Software, and to permit persons to whom the Software is", - "furnished to do so, subject to the following conditions:", - "", - "The above copyright notice and this permission notice shall be included in all", - "copies or substantial portions of the Software.", - "", - "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR", - "IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,", - "FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE", - "AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER", - "LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,", - "OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE", - "SOFTWARE." - ], - "isProd": true - }, - { - "name": "rust-lang/libc", - "version": "0.2.36", - "repositoryUrl": "https://github.com/rust-lang/libc", + "name": "rust-num/num-iter", + "version": "0.1.34", + "repositoryUrl": "https://github.com/rust-num/num-iter", "licenseDetail": [ "Copyright (c) 2014 The Rust Project Developers", "", @@ -283,9 +314,9 @@ "isProd": true }, { - "name": "rust-num/num-iter", - "version": "0.1.34", - "repositoryUrl": "https://github.com/rust-num/num-iter", + "name": "slog-rs/slog", + "version": "2.1.1", + "repositoryUrl": "https://github.com/slog-rs/slog", "licenseDetail": [ "Copyright (c) 2014 The Rust Project Developers", "", @@ -316,35 +347,60 @@ "isProd": true }, { - "name": "slog-rs/slog", - "version": "2.1.1", - "repositoryUrl": "https://github.com/slog-rs/slog", + "name": "Sgeo/take_mut", + "version": "0.2.0", + "repositoryUrl": "https://github.com/Sgeo/take_mut", "licenseDetail": [ - "Copyright (c) 2014 The Rust Project Developers", + "The MIT License (MIT)", "", - "Permission is hereby granted, free of charge, to any", - "person obtaining a copy of this software and associated", - "documentation files (the \"Software\"), to deal in the", - "Software without restriction, including without", - "limitation the rights to use, copy, modify, merge,", - "publish, distribute, sublicense, and/or sell copies of", - "the Software, and to permit persons to whom the Software", - "is furnished to do so, subject to the following", - "conditions:", + "Copyright (c) 2016 Sgeo", "", - "The above copyright notice and this permission notice", - "shall be included in all copies or substantial portions", - "of the Software.", + "Permission is hereby granted, free of charge, to any person obtaining a copy", + "of this software and associated documentation files (the \"Software\"), to deal", + "in the Software without restriction, including without limitation the rights", + "to use, copy, modify, merge, publish, distribute, sublicense, and/or sell", + "copies of the Software, and to permit persons to whom the Software is", + "furnished to do so, subject to the following conditions:", "", - "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF", - "ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED", - "TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A", - "PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT", - "SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY", - "CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION", - "OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR", - "IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER", - "DEALINGS IN THE SOFTWARE." + "The above copyright notice and this permission notice shall be included in all", + "copies or substantial portions of the Software.", + "", + "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR", + "IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,", + "FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE", + "AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER", + "LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,", + "OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE", + "SOFTWARE." + ], + "isProd": true + }, + { + "name": "redox-os/termios", + "version": "0.1.1", + "repositoryUrl": "https://github.com/redox-os/termios", + "licenseDetail": [ + "MIT License", + "", + "Copyright (c) 2017 Redox OS", + "", + "Permission is hereby granted, free of charge, to any person obtaining a copy", + "of this software and associated documentation files (the \"Software\"), to deal", + "in the Software without restriction, including without limitation the rights", + "to use, copy, modify, merge, publish, distribute, sublicense, and/or sell", + "copies of the Software, and to permit persons to whom the Software is", + "furnished to do so, subject to the following conditions:", + "", + "The above copyright notice and this permission notice shall be included in all", + "copies or substantial portions of the Software.", + "", + "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR", + "IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,", + "FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE", + "AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER", + "LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,", + "OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE", + "SOFTWARE." ], "isProd": true }, @@ -379,75 +435,13 @@ "isProd": true }, { - "name": "Stebalien/term", - "version": "0.4.6", - "repositoryUrl": "https://github.com/Stebalien/term", - "licenseDetail": [ - "Copyright (c) 2014 The Rust Project Developers", - "", - "Permission is hereby granted, free of charge, to any", - "person obtaining a copy of this software and associated", - "documentation files (the \"Software\"), to deal in the", - "Software without restriction, including without", - "limitation the rights to use, copy, modify, merge,", - "publish, distribute, sublicense, and/or sell copies of", - "the Software, and to permit persons to whom the Software", - "is furnished to do so, subject to the following", - "conditions:", - "", - "The above copyright notice and this permission notice", - "shall be included in all copies or substantial portions", - "of the Software.", - "", - "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF", - "ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED", - "TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A", - "PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT", - "SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY", - "CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION", - "OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR", - "IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER", - "DEALINGS IN THE SOFTWARE." - ], - "isProd": true - }, - { - "name": "redox-os/termios", - "version": "0.1.1", - "repositoryUrl": "https://github.com/redox-os/termios", + "name": "mrhooray/crc-rs", + "version": "1.7.0", + "repositoryUrl": "https://github.com/mrhooray/crc-rs.git", "licenseDetail": [ "MIT License", "", - "Copyright (c) 2017 Redox OS", - "", - "Permission is hereby granted, free of charge, to any person obtaining a copy", - "of this software and associated documentation files (the \"Software\"), to deal", - "in the Software without restriction, including without limitation the rights", - "to use, copy, modify, merge, publish, distribute, sublicense, and/or sell", - "copies of the Software, and to permit persons to whom the Software is", - "furnished to do so, subject to the following conditions:", - "", - "The above copyright notice and this permission notice shall be included in all", - "copies or substantial portions of the Software.", - "", - "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR", - "IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,", - "FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE", - "AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER", - "LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,", - "OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE", - "SOFTWARE." - ], - "isProd": true - }, - { - "name": "Sgeo/take_mut", - "version": "0.2.0", - "repositoryUrl": "https://github.com/Sgeo/take_mut", - "licenseDetail": [ - "The MIT License (MIT)", - "", - "Copyright (c) 2016 Sgeo", + "Copyright (c) 2017 crc-rs Developers", "", "Permission is hereby granted, free of charge, to any person obtaining a copy", "of this software and associated documentation files (the \"Software\"), to deal", @@ -560,39 +554,6 @@ ], "isProd": true }, - { - "name": "rust-lang/time", - "version": "0.1.39", - "repositoryUrl": "https://github.com/rust-lang/time", - "licenseDetail": [ - "Copyright (c) 2014 The Rust Project Developers", - "", - "Permission is hereby granted, free of charge, to any", - "person obtaining a copy of this software and associated", - "documentation files (the \"Software\"), to deal in the", - "Software without restriction, including without", - "limitation the rights to use, copy, modify, merge,", - "publish, distribute, sublicense, and/or sell copies of", - "the Software, and to permit persons to whom the Software", - "is furnished to do so, subject to the following", - "conditions:", - "", - "The above copyright notice and this permission notice", - "shall be included in all copies or substantial portions", - "of the Software.", - "", - "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF", - "ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED", - "TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A", - "PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT", - "SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY", - "CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION", - "OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR", - "IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER", - "DEALINGS IN THE SOFTWARE." - ], - "isProd": true - }, { "name": "chronotope/chrono", "version": "0.4.0", @@ -868,29 +829,35 @@ "isProd": true }, { - "name": "retep998/winapi-rs", - "version": "0.1.1", - "repositoryUrl": "https://github.com/retep998/winapi-rs", + "name": "rust-lang/time", + "version": "0.1.39", + "repositoryUrl": "https://github.com/rust-lang/time", "licenseDetail": [ - "Copyright (c) 2015 The winapi-rs Developers", + "Copyright (c) 2014 The Rust Project Developers", "", - "Permission is hereby granted, free of charge, to any person obtaining a copy", - "of this software and associated documentation files (the \"Software\"), to deal", - "in the Software without restriction, including without limitation the rights", - "to use, copy, modify, merge, publish, distribute, sublicense, and/or sell", - "copies of the Software, and to permit persons to whom the Software is", - "furnished to do so, subject to the following conditions:", + "Permission is hereby granted, free of charge, to any", + "person obtaining a copy of this software and associated", + "documentation files (the \"Software\"), to deal in the", + "Software without restriction, including without", + "limitation the rights to use, copy, modify, merge,", + "publish, distribute, sublicense, and/or sell copies of", + "the Software, and to permit persons to whom the Software", + "is furnished to do so, subject to the following", + "conditions:", "", - "The above copyright notice and this permission notice shall be included in all", - "copies or substantial portions of the Software.", + "The above copyright notice and this permission notice", + "shall be included in all copies or substantial portions", + "of the Software.", "", - "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR", - "IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,", - "FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE", - "AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER", - "LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,", - "OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE", - "SOFTWARE." + "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF", + "ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED", + "TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A", + "PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT", + "SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY", + "CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION", + "OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR", + "IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER", + "DEALINGS IN THE SOFTWARE." ], "isProd": true }, @@ -948,33 +915,6 @@ ], "isProd": true }, - { - "name": "retep998/winapi-rs", - "version": "0.4.0", - "repositoryUrl": "https://github.com/retep998/winapi-rs", - "licenseDetail": [ - "Copyright (c) 2015 The winapi-rs Developers", - "", - "Permission is hereby granted, free of charge, to any person obtaining a copy", - "of this software and associated documentation files (the \"Software\"), to deal", - "in the Software without restriction, including without limitation the rights", - "to use, copy, modify, merge, publish, distribute, sublicense, and/or sell", - "copies of the Software, and to permit persons to whom the Software is", - "furnished to do so, subject to the following conditions:", - "", - "The above copyright notice and this permission notice shall be included in all", - "copies or substantial portions of the Software.", - "", - "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR", - "IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,", - "FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE", - "AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER", - "LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,", - "OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE", - "SOFTWARE." - ], - "isProd": true - }, { "name": "retep998/winapi-rs", "version": "0.3.4", @@ -1002,33 +942,6 @@ ], "isProd": true }, - { - "name": "retep998/winapi-rs", - "version": "0.4.0", - "repositoryUrl": "https://github.com/retep998/winapi-rs", - "licenseDetail": [ - "Copyright (c) 2015 The winapi-rs Developers", - "", - "Permission is hereby granted, free of charge, to any person obtaining a copy", - "of this software and associated documentation files (the \"Software\"), to deal", - "in the Software without restriction, including without limitation the rights", - "to use, copy, modify, merge, publish, distribute, sublicense, and/or sell", - "copies of the Software, and to permit persons to whom the Software is", - "furnished to do so, subject to the following conditions:", - "", - "The above copyright notice and this permission notice shall be included in all", - "copies or substantial portions of the Software.", - "", - "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR", - "IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,", - "FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE", - "AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER", - "LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,", - "OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE", - "SOFTWARE." - ], - "isProd": true - }, { "name": "slog-rs/async", "version": "2.2.0", @@ -1790,5 +1703,92 @@ " defined by the Mozilla Public License, v. 2.0." ], "isProd": true + }, + { + "name": "retep998/winapi-rs", + "version": "0.1.1", + "repositoryUrl": "https://github.com/retep998/winapi-rs", + "licenseDetail": [ + "Copyright (c) 2015 The winapi-rs Developers", + "", + "Permission is hereby granted, free of charge, to any person obtaining a copy", + "of this software and associated documentation files (the \"Software\"), to deal", + "in the Software without restriction, including without limitation the rights", + "to use, copy, modify, merge, publish, distribute, sublicense, and/or sell", + "copies of the Software, and to permit persons to whom the Software is", + "furnished to do so, subject to the following conditions:", + "", + "The above copyright notice and this permission notice shall be included in all", + "copies or substantial portions of the Software.", + "", + "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR", + "IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,", + "FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE", + "AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER", + "LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,", + "OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE", + "SOFTWARE." + ], + "isProd": true + }, + { + "name": "retep998/winapi-rs", + "version": "0.4.0", + "repositoryUrl": "https://github.com/retep998/winapi-rs", + "licenseDetail": [ + "Copyright (c) 2015 The winapi-rs Developers", + "", + "Permission is hereby granted, free of charge, to any person obtaining a copy", + "of this software and associated documentation files (the \"Software\"), to deal", + "in the Software without restriction, including without limitation the rights", + "to use, copy, modify, merge, publish, distribute, sublicense, and/or sell", + "copies of the Software, and to permit persons to whom the Software is", + "furnished to do so, subject to the following conditions:", + "", + "The above copyright notice and this permission notice shall be included in all", + "copies or substantial portions of the Software.", + "", + "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR", + "IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,", + "FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE", + "AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER", + "LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,", + "OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE", + "SOFTWARE." + ], + "isProd": true + }, + { + "name": "Stebalien/term", + "version": "0.4.6", + "repositoryUrl": "https://github.com/Stebalien/term", + "licenseDetail": [ + "Copyright (c) 2014 The Rust Project Developers", + "", + "Permission is hereby granted, free of charge, to any", + "person obtaining a copy of this software and associated", + "documentation files (the \"Software\"), to deal in the", + "Software without restriction, including without", + "limitation the rights to use, copy, modify, merge,", + "publish, distribute, sublicense, and/or sell copies of", + "the Software, and to permit persons to whom the Software", + "is furnished to do so, subject to the following", + "conditions:", + "", + "The above copyright notice and this permission notice", + "shall be included in all copies or substantial portions", + "of the Software.", + "", + "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF", + "ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED", + "TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A", + "PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT", + "SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY", + "CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION", + "OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR", + "IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER", + "DEALINGS IN THE SOFTWARE." + ], + "isProd": true } ] diff --git a/build/win32/inno_updater.exe b/build/win32/inno_updater.exe index c216c404332..b69182a7e91 100755 Binary files a/build/win32/inno_updater.exe and b/build/win32/inno_updater.exe differ diff --git a/build/yarn.lock b/build/yarn.lock index ec3bd73e853..59bb057c34d 100644 --- a/build/yarn.lock +++ b/build/yarn.lock @@ -8,6 +8,10 @@ dependencies: "@types/node" "*" +"@types/caseless@*": + version "0.12.1" + resolved "https://registry.yarnpkg.com/@types/caseless/-/caseless-0.12.1.tgz#9794c69c8385d0192acc471a540d1f8e0d16218a" + "@types/documentdb@1.10.2": version "1.10.2" resolved "https://registry.yarnpkg.com/@types/documentdb/-/documentdb-1.10.2.tgz#6795025cdc51577af5ed531b6f03bd44404f5350" @@ -22,6 +26,12 @@ version "0.0.33" resolved "https://registry.yarnpkg.com/@types/es6-promise/-/es6-promise-0.0.33.tgz#280a707e62b1b6bef1a86cc0861ec63cd06c7ff3" +"@types/form-data@*": + version "2.2.1" + resolved "https://registry.yarnpkg.com/@types/form-data/-/form-data-2.2.1.tgz#ee2b3b8eaa11c0938289953606b745b738c54b1e" + dependencies: + "@types/node" "*" + "@types/mime@0.0.29": version "0.0.29" resolved "https://registry.yarnpkg.com/@types/mime/-/mime-0.0.29.tgz#fbcfd330573b912ef59eeee14602bface630754b" @@ -34,6 +44,19 @@ version "8.0.33" resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.33.tgz#1126e94374014e54478092830704f6ea89df04cd" +"@types/request@^2.47.0": + version "2.47.0" + resolved "https://registry.yarnpkg.com/@types/request/-/request-2.47.0.tgz#76a666cee4cb85dcffea6cd4645227926d9e114e" + dependencies: + "@types/caseless" "*" + "@types/form-data" "*" + "@types/node" "*" + "@types/tough-cookie" "*" + +"@types/tough-cookie@*": + version "2.3.2" + resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-2.3.2.tgz#e0d481d8bb282ad8a8c9e100ceb72c995fb5e709" + "@types/xml2js@0.0.33": version "0.0.33" resolved "https://registry.yarnpkg.com/@types/xml2js/-/xml2js-0.0.33.tgz#20c5dd6460245284d64a55690015b95e409fb7de" @@ -45,6 +68,15 @@ ajv@^4.9.1: co "^4.6.0" json-stable-stringify "^1.0.1" +ajv@^5.1.0: + version "5.5.2" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965" + dependencies: + co "^4.6.0" + fast-deep-equal "^1.0.0" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.3.0" + asn1@~0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.3.tgz#dac8787713c9966849fc8180777ebe9c1ddf3b86" @@ -65,10 +97,18 @@ aws-sign2@~0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f" +aws-sign2@~0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" + aws4@^1.2.1: version "1.6.0" resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e" +aws4@^1.6.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.7.0.tgz#d4d0e9b9dbfca77bf08eeb0a8a471550fe39e289" + azure-storage@^2.1.0: version "2.6.0" resolved "https://registry.yarnpkg.com/azure-storage/-/azure-storage-2.6.0.tgz#84747ee54a4bd194bb960f89f3eff89d67acf1cf" @@ -85,6 +125,10 @@ azure-storage@^2.1.0: xml2js "0.2.7" xmlbuilder "0.4.3" +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + bcrypt-pbkdf@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d" @@ -101,6 +145,25 @@ boom@2.x.x: dependencies: hoek "2.x.x" +boom@4.x.x: + version "4.3.1" + resolved "https://registry.yarnpkg.com/boom/-/boom-4.3.1.tgz#4f8a3005cb4a7e3889f749030fd25b96e01d2e31" + dependencies: + hoek "4.x.x" + +boom@5.x.x: + version "5.2.0" + resolved "https://registry.yarnpkg.com/boom/-/boom-5.2.0.tgz#5dd9da6ee3a5f302077436290cb717d3f4a54e02" + dependencies: + hoek "4.x.x" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + browserify-mime@~1.2.9: version "1.2.9" resolved "https://registry.yarnpkg.com/browserify-mime/-/browserify-mime-1.2.9.tgz#aeb1af28de6c0d7a6a2ce40adb68ff18422af31f" @@ -113,12 +176,26 @@ co@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" +colors@^1.1.2: + version "1.2.1" + resolved "https://registry.yarnpkg.com/colors/-/colors-1.2.1.tgz#f4a3d302976aaf042356ba1ade3b1a2c62d9d794" + +combined-stream@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.6.tgz#723e7df6e801ac5613113a7e445a9b69cb632818" + dependencies: + delayed-stream "~1.0.0" + combined-stream@^1.0.5, combined-stream@~1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.5.tgz#938370a57b4a51dea2c77c15d5c5fdf895164009" dependencies: delayed-stream "~1.0.0" +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" @@ -129,6 +206,12 @@ cryptiles@2.x.x: dependencies: boom "2.x.x" +cryptiles@3.x.x: + version "3.1.2" + resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-3.1.2.tgz#a89fbb220f5ce25ec56e8c4aa8a4fd7b5b0d29fe" + dependencies: + boom "5.x.x" + dashdash@^1.12.0: version "1.14.1" resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" @@ -158,7 +241,7 @@ extend@~1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/extend/-/extend-1.2.1.tgz#a0f5fd6cfc83a5fe49ef698d60ec8a624dd4576c" -extend@~3.0.0: +extend@~3.0.0, extend@~3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444" @@ -166,6 +249,14 @@ extsprintf@1.3.0, extsprintf@^1.2.0: version "1.3.0" resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" +fast-deep-equal@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz#c053477817c86b51daa853c81e059b733d023614" + +fast-json-stable-stringify@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" + forever-agent@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" @@ -178,16 +269,37 @@ form-data@~2.1.1: combined-stream "^1.0.5" mime-types "^2.1.12" +form-data@~2.3.1: + version "2.3.2" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.2.tgz#4970498be604c20c005d4f5c23aecd21d6b49099" + dependencies: + asynckit "^0.4.0" + combined-stream "1.0.6" + mime-types "^2.1.12" + getpass@^0.1.1: version "0.1.7" resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" dependencies: assert-plus "^1.0.0" +github-releases@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/github-releases/-/github-releases-0.4.1.tgz#4a13bdf85c4161344271db3d81db08e7379102ff" + dependencies: + minimatch "3.0.4" + optimist "0.6.1" + prettyjson "1.2.1" + request "2.81.0" + har-schema@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-1.0.5.tgz#d263135f43307c02c602afc8fe95970c0151369e" +har-schema@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" + har-validator@~4.2.1: version "4.2.1" resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-4.2.1.tgz#33481d0f1bbff600dd203d75812a6a5fba002e2a" @@ -195,6 +307,13 @@ har-validator@~4.2.1: ajv "^4.9.1" har-schema "^1.0.5" +har-validator@~5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.0.3.tgz#ba402c266194f15956ef15e0fcf242993f6a7dfd" + dependencies: + ajv "^5.1.0" + har-schema "^2.0.0" + hash-base@^3.0.0: version "3.0.4" resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.0.4.tgz#5fc8686847ecd73499403319a6b0a3f3f6ae4918" @@ -211,10 +330,23 @@ hawk@~3.1.3: hoek "2.x.x" sntp "1.x.x" +hawk@~6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/hawk/-/hawk-6.0.2.tgz#af4d914eb065f9b5ce4d9d11c1cb2126eecc3038" + dependencies: + boom "4.x.x" + cryptiles "3.x.x" + hoek "4.x.x" + sntp "2.x.x" + hoek@2.x.x: version "2.16.3" resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed" +hoek@4.x.x: + version "4.2.1" + resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.1.tgz#9634502aa12c445dd5a7c5734b572bb8738aacbb" + http-signature@~1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.1.1.tgz#df72e267066cd0ac67fb76adf8e134a8fbcf91bf" @@ -223,6 +355,14 @@ http-signature@~1.1.0: jsprim "^1.2.2" sshpk "^1.7.0" +http-signature@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" + dependencies: + assert-plus "^1.0.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + inherits@^2.0.1, inherits@~2.0.1: version "2.0.3" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" @@ -249,6 +389,10 @@ json-edm-parser@0.1.2: dependencies: jsonparse "~1.2.0" +json-schema-traverse@^0.3.0: + version "0.3.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340" + json-schema@0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" @@ -291,28 +435,66 @@ mime-db@~1.30.0: version "1.30.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.30.0.tgz#74c643da2dd9d6a45399963465b26d5ca7d71f01" +mime-db@~1.33.0: + version "1.33.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db" + mime-types@^2.1.12, mime-types@~2.1.7: version "2.1.17" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.17.tgz#09d7a393f03e995a79f8af857b70a9e0ab16557a" dependencies: mime-db "~1.30.0" +mime-types@~2.1.17: + version "2.1.18" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.18.tgz#6f323f60a83d11146f831ff11fd66e2fe5503bb8" + dependencies: + mime-db "~1.33.0" + mime@^1.3.4: version "1.4.1" resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6" +minimatch@3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + dependencies: + brace-expansion "^1.1.7" + minimist@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" -oauth-sign@~0.8.1: +minimist@~0.0.1: + version "0.0.10" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" + +oauth-sign@~0.8.1, oauth-sign@~0.8.2: version "0.8.2" resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" +optimist@0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" + dependencies: + minimist "~0.0.1" + wordwrap "~0.0.2" + performance-now@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5" +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + +prettyjson@1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/prettyjson/-/prettyjson-1.2.1.tgz#fcffab41d19cab4dfae5e575e64246619b12d289" + dependencies: + colors "^1.1.2" + minimist "^1.2.0" + priorityqueuejs@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/priorityqueuejs/-/priorityqueuejs-1.0.0.tgz#2ee4f23c2560913e08c07ce5ccdd6de3df2c5af8" @@ -329,6 +511,10 @@ qs@~6.4.0: version "6.4.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" +qs@~6.5.1: + version "6.5.1" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8" + readable-stream@~2.0.0: version "2.0.6" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.0.6.tgz#8f90341e68a53ccc928788dacfcd11b36eb9b78e" @@ -340,7 +526,7 @@ readable-stream@~2.0.0: string_decoder "~0.10.x" util-deprecate "~1.0.1" -request@~2.81.0: +request@2.81.0, request@~2.81.0: version "2.81.0" resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0" dependencies: @@ -367,7 +553,34 @@ request@~2.81.0: tunnel-agent "^0.6.0" uuid "^3.0.0" -safe-buffer@^5.0.1: +request@^2.85.0: + version "2.85.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.85.0.tgz#5a03615a47c61420b3eb99b7dba204f83603e1fa" + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.6.0" + caseless "~0.12.0" + combined-stream "~1.0.5" + extend "~3.0.1" + forever-agent "~0.6.1" + form-data "~2.3.1" + har-validator "~5.0.3" + hawk "~6.0.2" + http-signature "~1.2.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.17" + oauth-sign "~0.8.2" + performance-now "^2.1.0" + qs "~6.5.1" + safe-buffer "^5.1.1" + stringstream "~0.0.5" + tough-cookie "~2.3.3" + tunnel-agent "^0.6.0" + uuid "^3.1.0" + +safe-buffer@^5.0.1, safe-buffer@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" @@ -389,6 +602,12 @@ sntp@1.x.x: dependencies: hoek "2.x.x" +sntp@2.x.x: + version "2.1.0" + resolved "https://registry.yarnpkg.com/sntp/-/sntp-2.1.0.tgz#2c6cec14fedc2222739caf9b5c3d85d1cc5a2cc8" + dependencies: + hoek "4.x.x" + sshpk@^1.7.0: version "1.13.1" resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.13.1.tgz#512df6da6287144316dc4c18fe1cf1d940739be3" @@ -407,7 +626,7 @@ string_decoder@~0.10.x: version "0.10.31" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" -stringstream@~0.0.4: +stringstream@~0.0.4, stringstream@~0.0.5: version "0.0.5" resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878" @@ -417,6 +636,12 @@ tough-cookie@~2.3.0: dependencies: punycode "^1.4.1" +tough-cookie@~2.3.3: + version "2.3.4" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.4.tgz#ec60cee38ac675063ffc97a5c18970578ee83655" + dependencies: + punycode "^1.4.1" + tunnel-agent@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" @@ -427,9 +652,9 @@ tweetnacl@^0.14.3, tweetnacl@~0.14.0: version "0.14.5" resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" -typescript@2.7.2: - version "2.7.2" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.7.2.tgz#2d615a1ef4aee4f574425cdff7026edf81919836" +typescript@2.8.1: + version "2.8.1" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.8.1.tgz#6160e4f8f195d5ba81d4876f9c0cc1fbc0820624" underscore@1.8.3, underscore@~1.8.3: version "1.8.3" @@ -443,6 +668,10 @@ uuid@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04" +uuid@^3.1.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.2.1.tgz#12c528bb9d58d0b9265d9a2f6f0fe8be17ff1f14" + validator@~3.35.0: version "3.35.0" resolved "https://registry.yarnpkg.com/validator/-/validator-3.35.0.tgz#3f07249402c1fc8fc093c32c6e43d72a79cca1dc" @@ -455,6 +684,10 @@ verror@1.10.0: core-util-is "1.0.2" extsprintf "^1.2.0" +wordwrap@~0.0.2: + version "0.0.3" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" + xml2js@0.2.7: version "0.2.7" resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.2.7.tgz#1838518bb01741cae0878bab4915e494c32306af" diff --git a/extensions/bat/syntaxes/batchfile.tmLanguage.json b/extensions/bat/syntaxes/batchfile.tmLanguage.json index 996d5aed4da..e5f00ed3827 100644 --- a/extensions/bat/syntaxes/batchfile.tmLanguage.json +++ b/extensions/bat/syntaxes/batchfile.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/mmims/language-batchfile/commit/9b2d868a43d6a04a4dc27cb31f907b960a4fdab1", + "version": "https://github.com/mmims/language-batchfile/commit/6235c491be4dff49cd3966b50142874d7f79580a", "name": "Batch File", "scopeName": "source.batchfile", "patterns": [ @@ -46,14 +46,17 @@ "commands": { "patterns": [ { - "match": "(?<=^|[\\s@])(?i:adprep|append|arp|assoc|at|atmadm|attrib|auditpol|autochk|autoconv|autofmt|bcdboot|bcdedit|bdehdcfg|bitsadmin|bootcfg|brea|cacls|cd|certreq|certutil|change|chcp|chdir|chglogon|chgport|chgusr|chkdsk|chkntfs|choice|cipher|clip|clscluadmin|cluster|cmd|cmdkey|cmstp|color|comp|compact|convert|copy|cprofile|cscript|csvde|date|dcdiag|dcgpofix|dcpromo|defra|del|dfscmd|dfsdiag|dfsrmig|diantz|dir|dirquota|diskcomp|diskcopy|diskpart|diskperf|diskraid|diskshadow|dispdiag|doin|dnscmd|doskey|driverquery|dsacls|dsadd|dsamain|dsdbutil|dsget|dsmgmt|dsmod|dsmove|dsquery|dsrm|edit|endlocal|eraseesentutl|eventcreate|eventquery|eventtriggers|evntcmd|expand|extract|fc|filescrn|find|findstr|finger|flattemp|fonde|forfiles|format|freedisk|fsutil|ftp|ftype|fveupdate|getmac|gettype|gpfixup|gpresult|gpupdate|graftabl|hashgen|hep|helpctr|hostname|icacls|iisreset|inuse|ipconfig|ipxroute|irftp|ismserv|jetpack|klist|ksetup|ktmutil|ktpass|label|ldifd|ldp|lodctr|logman|logoff|lpq|lpr|macfile|makecab|manage-bde|mapadmin|md|mkdir|mklink|mmc|mode|more|mount|mountvol|move|mqbup|mqsvc|mqtgsvc|msdt|msg|msiexec|msinfo32|mstsc|nbtstat|net computer|net group|net localgroup|net print|net session|net share|net start|net stop|net use|net user|net view|net|netcfg|netdiag|netdom|netsh|netstat|nfsadmin|nfsshare|nfsstat|nlb|nlbmgr|nltest|nslookup|ntackup|ntcmdprompt|ntdsutil|ntfrsutl|openfiles|pagefileconfig|path|pathping|pause|pbadmin|pentnt|perfmon|ping|pnpunatten|pnputil|popd|powercfg|powershell|powershell_ise|print|prncnfg|prndrvr|prnjobs|prnmngr|prnport|prnqctl|prompt|pubprn|pushd|pushprinterconnections|pwlauncher|qappsrv|qprocess|query|quser|qwinsta|rasdial|rcp|rd|rdpsign|regentc|recover|redircmp|redirusr|reg|regini|regsvr32|relog|ren|rename|rendom|repadmin|repair-bde|replace|reset session|rxec|risetup|rmdir|robocopy|route|rpcinfo|rpcping|rsh|runas|rundll32|rwinsta|sc|schtasks|scwcmd|secedit|serverceipoptin|servrmanagercmd|serverweroptin|setlocal|setspn|setx|sfc|shadow|shift|showmount|shutdown|sort|start|storrept|subst|sxstrace|ysocmgr|systeminfo|takeown|tapicfg|taskkill|tasklist|tcmsetup|telnet|tftp|time|timeout|title|tlntadmn|tpmvscmgr|tpmvscmgr|tacerpt|tracert|tree|tscon|tsdiscon|tsecimp|tskill|tsprof|type|typeperf|tzutil|uddiconfig|umount|unlodctr|ver|verifier|verif|vol|vssadmin|w32tm|waitfor|wbadmin|wdsutil|wecutil|wevtutil|where|whoami|winnt|winnt32|winpop|winrm|winrs|winsat|wlbs|mic|wscript|xcopy)(?=$|\\s)", + "match": "(?<=^|[\\s@])(?i:adprep|append|arp|assoc|at|atmadm|attrib|auditpol|autochk|autoconv|autofmt|bcdboot|bcdedit|bdehdcfg|bitsadmin|bootcfg|brea|cacls|cd|certreq|certutil|change|chcp|chdir|chglogon|chgport|chgusr|chkdsk|chkntfs|choice|cipher|clip|cls|clscluadmin|cluster|cmd|cmdkey|cmstp|color|comp|compact|convert|copy|cprofile|cscript|csvde|date|dcdiag|dcgpofix|dcpromo|defra|del|dfscmd|dfsdiag|dfsrmig|diantz|dir|dirquota|diskcomp|diskcopy|diskpart|diskperf|diskraid|diskshadow|dispdiag|doin|dnscmd|doskey|driverquery|dsacls|dsadd|dsamain|dsdbutil|dsget|dsmgmt|dsmod|dsmove|dsquery|dsrm|edit|endlocal|eraseesentutl|eventcreate|eventquery|eventtriggers|evntcmd|expand|extract|fc|filescrn|find|findstr|finger|flattemp|fonde|forfiles|format|freedisk|fsutil|ftp|ftype|fveupdate|getmac|gettype|gpfixup|gpresult|gpupdate|graftabl|hashgen|hep|helpctr|hostname|icacls|iisreset|inuse|ipconfig|ipxroute|irftp|ismserv|jetpack|klist|ksetup|ktmutil|ktpass|label|ldifd|ldp|lodctr|logman|logoff|lpq|lpr|macfile|makecab|manage-bde|mapadmin|md|mkdir|mklink|mmc|mode|more|mount|mountvol|move|mqbup|mqsvc|mqtgsvc|msdt|msg|msiexec|msinfo32|mstsc|nbtstat|net computer|net group|net localgroup|net print|net session|net share|net start|net stop|net use|net user|net view|net|netcfg|netdiag|netdom|netsh|netstat|nfsadmin|nfsshare|nfsstat|nlb|nlbmgr|nltest|nslookup|ntackup|ntcmdprompt|ntdsutil|ntfrsutl|openfiles|pagefileconfig|path|pathping|pause|pbadmin|pentnt|perfmon|ping|pnpunatten|pnputil|popd|powercfg|powershell|powershell_ise|print|prncnfg|prndrvr|prnjobs|prnmngr|prnport|prnqctl|prompt|pubprn|pushd|pushprinterconnections|pwlauncher|qappsrv|qprocess|query|quser|qwinsta|rasdial|rcp|rd|rdpsign|regentc|recover|redircmp|redirusr|reg|regini|regsvr32|relog|ren|rename|rendom|repadmin|repair-bde|replace|reset session|rxec|risetup|rmdir|robocopy|route|rpcinfo|rpcping|rsh|runas|rundll32|rwinsta|sc|schtasks|scwcmd|secedit|serverceipoptin|servrmanagercmd|serverweroptin|setspn|setx|sfc|shadow|shift|showmount|shutdown|sort|start|storrept|subst|sxstrace|ysocmgr|systeminfo|takeown|tapicfg|taskkill|tasklist|tcmsetup|telnet|tftp|time|timeout|title|tlntadmn|tpmvscmgr|tpmvscmgr|tacerpt|tracert|tree|tscon|tsdiscon|tsecimp|tskill|tsprof|type|typeperf|tzutil|uddiconfig|umount|unlodctr|ver|verifier|verif|vol|vssadmin|w32tm|waitfor|wbadmin|wdsutil|wecutil|wevtutil|where|whoami|winnt|winnt32|winpop|winrm|winrs|winsat|wlbs|mic|wscript|xcopy)(?=$|\\s)", "name": "keyword.command.batchfile" }, { - "begin": "(?<=^|[\\s@])(?i:echo)(?=$|\\s|\\.)", + "begin": "(?i)(?<=^|[\\s@])(echo)(?:(?=$|\\.|:)|\\s+(?:(on|off)(?=\\s*$))?)", "beginCaptures": { - "0": { + "1": { "name": "keyword.command.batchfile" + }, + "2": { + "name": "keyword.other.special-method.batchfile" } }, "end": "(?=$\\n|[&|><)])", @@ -72,6 +75,17 @@ } ] }, + { + "match": "(?i)(?<=^|[\\s@])(setlocal)(?:\\s*$|\\s+(EnableExtensions|DisableExtensions|EnableDelayedExpansion|DisableDelayedExpansion)(?=\\s*$))", + "captures": { + "1": { + "name": "keyword.command.batchfile" + }, + "2": { + "name": "keyword.other.special-method.batchfile" + } + } + }, { "include": "#command_set" } @@ -110,36 +124,7 @@ "include": "#parens" }, { - "begin": "(\")\\s*([^ ][^=]*)(=)", - "beginCaptures": { - "1": { - "name": "punctuation.definition.string.begin.batchfile" - }, - "2": { - "name": "variable.other.readwrite.batchfile" - }, - "3": { - "name": "keyword.operator.assignment.batchfile" - } - }, - "end": "\"", - "endCaptures": { - "0": { - "name": "punctuation.definition.string.end.batchfile" - } - }, - "name": "string.quoted.double.batchfile", - "patterns": [ - { - "include": "#variables" - }, - { - "include": "#numbers" - }, - { - "include": "#parens" - } - ] + "include": "#command_set_strings" }, { "include": "#strings" @@ -216,6 +201,9 @@ "begin": "\\s+/[pP]\\s+", "end": "(?=$\\n|[&|><)])", "patterns": [ + { + "include": "#command_set_strings" + }, { "begin": "([^ ][^=]*)(=)", "beginCaptures": { @@ -295,6 +283,42 @@ } ] }, + "command_set_strings": { + "patterns": [ + { + "begin": "(\")\\s*([^ ][^=]*)(=)", + "beginCaptures": { + "1": { + "name": "punctuation.definition.string.begin.batchfile" + }, + "2": { + "name": "variable.other.readwrite.batchfile" + }, + "3": { + "name": "keyword.operator.assignment.batchfile" + } + }, + "end": "\"", + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.batchfile" + } + }, + "name": "string.quoted.double.batchfile", + "patterns": [ + { + "include": "#variables" + }, + { + "include": "#numbers" + }, + { + "include": "#escaped_characters" + } + ] + } + ] + }, "comments": { "patterns": [ { @@ -360,7 +384,7 @@ "controls": { "patterns": [ { - "match": "(?<=^|\\s)(?i)(?:goto|call|exit)(?=$|\\s)", + "match": "(?i)(?<=^|\\s)(?:call|exit(?=$|\\s)|goto(?=$|\\s|:))", "name": "keyword.control.statement.batchfile" }, { @@ -390,7 +414,7 @@ "escaped_characters": { "patterns": [ { - "match": "%%|\\^\\^!|\\^.|\\^\\n", + "match": "%%|\\^\\^!|\\^(?=.)|\\^\\n", "name": "constant.character.escape.batchfile" } ] @@ -398,7 +422,7 @@ "labels": { "patterns": [ { - "match": "^\\s*(:)([^+=,;:\\s].*)$", + "match": "(?i)(?:^\\s*|(?<=goto)\\s*)(:)([^+=,;:\\s].*)$", "captures": { "1": { "name": "punctuation.separator.batchfile" @@ -433,11 +457,11 @@ "name": "keyword.operator.logical.batchfile" }, { - "match": "&&?|\\|\\|", + "match": "(? -1) { const fixPosition = document.positionAt(lastEndOfSomething); - edit.insert(document.uri, fixPosition, ','); + + // Don't insert a comma immediately before a : or ' :' + const colonRange = document.getWordRangeAtPosition(fixPosition, / *:/); + if (!colonRange) { + edit.insert(document.uri, fixPosition, ','); + } } } }); diff --git a/extensions/cpp/language-configuration.json b/extensions/cpp/language-configuration.json index 23d99a485e1..e50df32dd11 100644 --- a/extensions/cpp/language-configuration.json +++ b/extensions/cpp/language-configuration.json @@ -20,7 +20,8 @@ ["[", "]"], ["(", ")"], ["\"", "\""], - ["'", "'"] + ["'", "'"], + ["<", ">"] ], "folding": { "markers": { diff --git a/extensions/csharp/syntaxes/csharp.tmLanguage.json b/extensions/csharp/syntaxes/csharp.tmLanguage.json index fb3f667f5b1..397c2712e03 100644 --- a/extensions/csharp/syntaxes/csharp.tmLanguage.json +++ b/extensions/csharp/syntaxes/csharp.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/dotnet/csharp-tmLanguage/commit/ca22c5211b87a6a1a8fd5356895237bb821df728", + "version": "https://github.com/dotnet/csharp-tmLanguage/commit/7689494edad006eafb9025aa6d72f8a634011a00", "name": "C#", "scopeName": "source.cs", "patterns": [ @@ -2050,6 +2050,9 @@ "patterns": [ { "include": "#block" + }, + { + "include": "#comment" } ] }, @@ -3670,7 +3673,7 @@ } }, "type-builtin": { - "match": "\\b(bool|byte|char|decimal|double|float|int|long|object|sbyte|short|string|uint|ulong|ushort|void)\\b", + "match": "\\b(bool|byte|char|decimal|double|float|int|long|object|sbyte|short|string|uint|ulong|ushort|void|dynamic)\\b", "captures": { "1": { "name": "keyword.type.cs" @@ -4131,7 +4134,7 @@ }, { "name": "comment.line.double-slash.cs", - "begin": "(? { + context.subscriptions.push(initCompletionProvider()); + context.subscriptions.push(initFoldingProvider()); }); + function initCompletionProvider(): Disposable { + const regionCompletionRegExpr = /^(\s*)(\/(\*\s*(#\w*)?)?)?$/; + + return languages.registerCompletionItemProvider(documentSelector, { + provideCompletionItems(doc, pos) { + let lineUntilPos = doc.getText(new Range(new Position(pos.line, 0), pos)); + let match = lineUntilPos.match(regionCompletionRegExpr); + if (match) { + let range = new Range(new Position(pos.line, match[1].length), pos); + let beginProposal = new CompletionItem('#region', CompletionItemKind.Snippet); + beginProposal.range = range; TextEdit.replace(range, '/* #region */'); + beginProposal.insertText = new SnippetString('/* #region $1*/'); + beginProposal.documentation = localize('folding.start', 'Folding Region Start'); + beginProposal.filterText = match[2]; + beginProposal.sortText = 'za'; + let endProposal = new CompletionItem('#endregion', CompletionItemKind.Snippet); + endProposal.range = range; + endProposal.insertText = '/* #endregion */'; + endProposal.documentation = localize('folding.end', 'Folding Region End'); + endProposal.sortText = 'zb'; + endProposal.filterText = match[2]; + return [beginProposal, endProposal]; + } + return null; + } + }); + } + + function initFoldingProvider(): Disposable { + const kinds: { [value: string]: FoldingRangeKind } = Object.create(null); + function getKind(value: string | undefined) { + if (!value) { + return void 0; + } + let kind = kinds[value]; + if (!kind) { + kind = new FoldingRangeKind(value); + kinds[value] = kind; + } + return kind; + } + return languages.registerFoldingRangeProvider(documentSelector, { + provideFoldingRanges(document: TextDocument, context: FoldingContext, token: CancellationToken) { + const param: FoldingRangeRequestParam = { + textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(document) + }; + return client.sendRequest(FoldingRangeRequest.type, param, token).then(ranges => { + if (Array.isArray(ranges)) { + return ranges.map(r => new FoldingRange(r.startLine, r.endLine, getKind(r.kind))); + } + return null; + }, error => { + client.logFailedRequest(FoldingRangeRequest.type, error); + return null; + }); + } + }); + } + commands.registerCommand('_css.applyCodeAction', applyCodeAction); function applyCodeAction(uri: string, documentVersion: number, edits: TextEdit[]) { diff --git a/extensions/css-language-features/client/tsconfig.json b/extensions/css-language-features/client/tsconfig.json index ee67f8333d4..473f4e85fc9 100644 --- a/extensions/css-language-features/client/tsconfig.json +++ b/extensions/css-language-features/client/tsconfig.json @@ -4,6 +4,7 @@ "module": "commonjs", "outDir": "./out", "noUnusedLocals": true, + "sourceMap": true, "lib": [ "es2016" ], diff --git a/extensions/css-language-features/package.json b/extensions/css-language-features/package.json index deb2c755d7a..4179c9fef2c 100644 --- a/extensions/css-language-features/package.json +++ b/extensions/css-language-features/package.json @@ -1,7 +1,7 @@ { "name": "css-language-features", - "displayName": "%displayName%", - "description": "%description%", + "displayName": "%displayName%", + "description": "%description%", "version": "1.0.0", "publisher": "vscode", "engines": { @@ -21,6 +21,9 @@ "postinstall": "cd server && yarn install", "install-client-next": "yarn add vscode-languageclient@next" }, + "categories": [ + "Programming Languages" + ], "contributes": { "configuration": [ { @@ -692,7 +695,8 @@ ] }, "dependencies": { - "vscode-languageclient": "4.0.0-next.9", + "vscode-languageclient": "^4.0.0", + "vscode-languageserver-protocol-foldingprovider": "^2.0.0-next.2", "vscode-nls": "^3.2.1" }, "devDependencies": { diff --git a/extensions/css-language-features/server/package.json b/extensions/css-language-features/server/package.json index 26769bfeb61..97379871be5 100644 --- a/extensions/css-language-features/server/package.json +++ b/extensions/css-language-features/server/package.json @@ -8,9 +8,9 @@ "node": "*" }, "dependencies": { - "vscode-css-languageservice": "^3.0.9-next.1", - "vscode-emmet-helper": "^1.2.0", - "vscode-languageserver": "^4.0.0" + "vscode-css-languageservice": "^3.0.9-next.10", + "vscode-languageserver": "^4.0.0", + "vscode-languageserver-protocol-foldingprovider": "^2.0.0-next.2" }, "devDependencies": { "@types/mocha": "2.2.33", diff --git a/extensions/css-language-features/server/src/cssServerMain.ts b/extensions/css-language-features/server/src/cssServerMain.ts index 66b473f1a6d..b78b9e4e40d 100644 --- a/extensions/css-language-features/server/src/cssServerMain.ts +++ b/extensions/css-language-features/server/src/cssServerMain.ts @@ -5,17 +5,17 @@ 'use strict'; import { - createConnection, IConnection, TextDocuments, InitializeParams, InitializeResult, ServerCapabilities, - ConfigurationRequest, WorkspaceFolder, DocumentColorRequest, ColorPresentationRequest + createConnection, IConnection, TextDocuments, InitializeParams, InitializeResult, ServerCapabilities, ConfigurationRequest, WorkspaceFolder } from 'vscode-languageserver'; import { TextDocument, CompletionList } from 'vscode-languageserver-types'; import { getCSSLanguageService, getSCSSLanguageService, getLESSLanguageService, LanguageSettings, LanguageService, Stylesheet } from 'vscode-css-languageservice'; import { getLanguageModelCache } from './languageModelCache'; -import { formatError, runSafe } from './utils/errors'; +import { formatError, runSafe } from './utils/runner'; import URI from 'vscode-uri'; import { getPathCompletionParticipant } from './pathCompletion'; +import { FoldingRangeServerCapabilities, FoldingRangeRequest } from 'vscode-languageserver-protocol-foldingprovider'; export interface Settings { css: LanguageSettings; @@ -49,9 +49,10 @@ connection.onShutdown(() => { }); let scopedSettingsSupport = false; -let workspaceFolders: WorkspaceFolder[] | undefined; +let foldingRangeLimit = Number.MAX_VALUE; +let workspaceFolders: WorkspaceFolder[]; -// After the server has started the client sends an initilize request. The server receives +// After the server has started the client sends an initialize request. The server receives // in the passed params the rootPath of the workspace plus the client capabilities. connection.onInitialize((params: InitializeParams): InitializeResult => { workspaceFolders = (params).workspaceFolders; @@ -62,20 +63,25 @@ connection.onInitialize((params: InitializeParams): InitializeResult => { } } - function hasClientCapability(name: string) { + function getClientCapability(name: string, def: T) { let keys = name.split('.'); let c: any = params.capabilities; for (let i = 0; c && i < keys.length; i++) { + if (!c.hasOwnProperty(keys[i])) { + return def; + } c = c[keys[i]]; } - return !!c; + return c; } - let snippetSupport = hasClientCapability('textDocument.completion.completionItem.snippetSupport'); - scopedSettingsSupport = hasClientCapability('workspace.configuration'); - let capabilities: ServerCapabilities = { + let snippetSupport = !!getClientCapability('textDocument.completion.completionItem.snippetSupport', false); + scopedSettingsSupport = !!getClientCapability('workspace.configuration', false); + foldingRangeLimit = getClientCapability('textDocument.foldingRange.rangeLimit', Number.MAX_VALUE); + + let capabilities: ServerCapabilities & FoldingRangeServerCapabilities = { // Tell the client that the server works in FULL text document sync mode textDocumentSync: documents.syncKind, - completionProvider: snippetSupport ? { resolveProvider: false } : undefined, + completionProvider: snippetSupport ? { resolveProvider: false, triggerCharacters: ['/'] } : undefined, hoverProvider: true, documentSymbolProvider: true, referencesProvider: true, @@ -83,7 +89,8 @@ connection.onInitialize((params: InitializeParams): InitializeResult => { documentHighlightProvider: true, codeActionProvider: true, renameProvider: true, - colorProvider: true + colorProvider: true, + foldingRangeProvider: true }; return { capabilities }; }); @@ -179,7 +186,7 @@ function validateTextDocument(textDocument: TextDocument): void { }); } -connection.onCompletion(textDocumentPosition => { +connection.onCompletion((textDocumentPosition, token) => { return runSafe(() => { let document = documents.get(textDocumentPosition.textDocument.uri); const cssLS = getLanguageService(document); @@ -190,61 +197,61 @@ connection.onCompletion(textDocumentPosition => { cssLS.setCompletionParticipants([getPathCompletionParticipant(document, workspaceFolders, pathCompletionList)]); const result = cssLS.doComplete(document, textDocumentPosition.position, stylesheets.get(document))!; /* TODO: remove ! once LS has null annotations */ return { - isIncomplete: result.isIncomplete, + isIncomplete: pathCompletionList.isIncomplete, items: [...pathCompletionList.items, ...result.items] }; - }, null, `Error while computing completions for ${textDocumentPosition.textDocument.uri}`); + }, null, `Error while computing completions for ${textDocumentPosition.textDocument.uri}`, token); }); -connection.onHover(textDocumentPosition => { +connection.onHover((textDocumentPosition, token) => { return runSafe(() => { let document = documents.get(textDocumentPosition.textDocument.uri); let styleSheet = stylesheets.get(document); - return getLanguageService(document).doHover(document, textDocumentPosition.position, styleSheet)!; /* TODO: remove ! once LS has null annotations */ - }, null, `Error while computing hover for ${textDocumentPosition.textDocument.uri}`); + return getLanguageService(document).doHover(document, textDocumentPosition.position, styleSheet); + }, null, `Error while computing hover for ${textDocumentPosition.textDocument.uri}`, token); }); -connection.onDocumentSymbol(documentSymbolParams => { +connection.onDocumentSymbol((documentSymbolParams, token) => { return runSafe(() => { let document = documents.get(documentSymbolParams.textDocument.uri); let stylesheet = stylesheets.get(document); return getLanguageService(document).findDocumentSymbols(document, stylesheet); - }, [], `Error while computing document symbols for ${documentSymbolParams.textDocument.uri}`); + }, [], `Error while computing document symbols for ${documentSymbolParams.textDocument.uri}`, token); }); -connection.onDefinition(documentSymbolParams => { +connection.onDefinition((documentSymbolParams, token) => { return runSafe(() => { let document = documents.get(documentSymbolParams.textDocument.uri); let stylesheet = stylesheets.get(document); return getLanguageService(document).findDefinition(document, documentSymbolParams.position, stylesheet); - }, null, `Error while computing definitions for ${documentSymbolParams.textDocument.uri}`); + }, null, `Error while computing definitions for ${documentSymbolParams.textDocument.uri}`, token); }); -connection.onDocumentHighlight(documentSymbolParams => { +connection.onDocumentHighlight((documentSymbolParams, token) => { return runSafe(() => { let document = documents.get(documentSymbolParams.textDocument.uri); let stylesheet = stylesheets.get(document); return getLanguageService(document).findDocumentHighlights(document, documentSymbolParams.position, stylesheet); - }, [], `Error while computing document highlights for ${documentSymbolParams.textDocument.uri}`); + }, [], `Error while computing document highlights for ${documentSymbolParams.textDocument.uri}`, token); }); -connection.onReferences(referenceParams => { +connection.onReferences((referenceParams, token) => { return runSafe(() => { let document = documents.get(referenceParams.textDocument.uri); let stylesheet = stylesheets.get(document); return getLanguageService(document).findReferences(document, referenceParams.position, stylesheet); - }, [], `Error while computing references for ${referenceParams.textDocument.uri}`); + }, [], `Error while computing references for ${referenceParams.textDocument.uri}`, token); }); -connection.onCodeAction(codeActionParams => { +connection.onCodeAction((codeActionParams, token) => { return runSafe(() => { let document = documents.get(codeActionParams.textDocument.uri); let stylesheet = stylesheets.get(document); return getLanguageService(document).doCodeActions(document, codeActionParams.range, codeActionParams.context, stylesheet); - }, [], `Error while computing code actions for ${codeActionParams.textDocument.uri}`); + }, [], `Error while computing code actions for ${codeActionParams.textDocument.uri}`, token); }); -connection.onRequest(DocumentColorRequest.type, params => { +connection.onDocumentColor((params, token) => { return runSafe(() => { let document = documents.get(params.textDocument.uri); if (document) { @@ -252,10 +259,10 @@ connection.onRequest(DocumentColorRequest.type, params => { return getLanguageService(document).findDocumentColors(document, stylesheet); } return []; - }, [], `Error while computing document colors for ${params.textDocument.uri}`); + }, [], `Error while computing document colors for ${params.textDocument.uri}`, token); }); -connection.onRequest(ColorPresentationRequest.type, params => { +connection.onColorPresentation((params, token) => { return runSafe(() => { let document = documents.get(params.textDocument.uri); if (document) { @@ -263,15 +270,22 @@ connection.onRequest(ColorPresentationRequest.type, params => { return getLanguageService(document).getColorPresentations(document, stylesheet, params.color, params.range); } return []; - }, [], `Error while computing color presentations for ${params.textDocument.uri}`); + }, [], `Error while computing color presentations for ${params.textDocument.uri}`, token); }); -connection.onRenameRequest(renameParameters => { +connection.onRenameRequest((renameParameters, token) => { return runSafe(() => { let document = documents.get(renameParameters.textDocument.uri); let stylesheet = stylesheets.get(document); return getLanguageService(document).doRename(document, renameParameters.position, renameParameters.newName, stylesheet); - }, null, `Error while computing renames for ${renameParameters.textDocument.uri}`); + }, null, `Error while computing renames for ${renameParameters.textDocument.uri}`, token); +}); + +connection.onRequest(FoldingRangeRequest.type, (params, token) => { + return runSafe(() => { + let document = documents.get(params.textDocument.uri); + return getLanguageService(document).getFoldingRanges(document, { rangeLimit: foldingRangeLimit }); + }, null, `Error while computing folding ranges for ${params.textDocument.uri}`, token); }); // Listen on the connection diff --git a/extensions/css-language-features/server/src/pathCompletion.ts b/extensions/css-language-features/server/src/pathCompletion.ts index 76b93f3a628..b072c4136f6 100644 --- a/extensions/css-language-features/server/src/pathCompletion.ts +++ b/extensions/css-language-features/server/src/pathCompletion.ts @@ -16,74 +16,65 @@ import { startsWith } from './utils/strings'; export function getPathCompletionParticipant( document: TextDocument, - workspaceFolders: WorkspaceFolder[] | undefined, + workspaceFolders: WorkspaceFolder[], result: CompletionList ): ICompletionParticipant { return { - onURILiteralValue: (context: { uriValue: string, position: Position, range: Range; }) => { + onCssURILiteralValue: ({ position, range, uriValue }) => { + const isValueQuoted = startsWith(uriValue, `'`) || startsWith(uriValue, `"`); + const fullValue = stripQuotes(uriValue); + const valueBeforeCursor = isValueQuoted + ? fullValue.slice(0, position.character - (range.start.character + 1)) + : fullValue.slice(0, position.character - range.start.character); + + if (fullValue === '.' || fullValue === '..') { + result.isIncomplete = true; + return; + } + if (!workspaceFolders || workspaceFolders.length === 0) { return; } const workspaceRoot = resolveWorkspaceRoot(document, workspaceFolders); + const paths = providePaths(valueBeforeCursor, URI.parse(document.uri).fsPath, workspaceRoot); - // Handle quoted values - let uriValue = context.uriValue; - let range = context.range; - if (startsWith(uriValue, `'`) || startsWith(uriValue, `"`)) { - uriValue = uriValue.slice(1, -1); - range = getRangeWithoutQuotes(range); - } - - const suggestions = providePathSuggestions(uriValue, range, URI.parse(document.uri).fsPath, workspaceRoot); + const fullValueRange = isValueQuoted ? shiftRange(range, 1, -1) : range; + const replaceRange = pathToReplaceRange(valueBeforeCursor, fullValue, fullValueRange); + const suggestions = paths.map(p => pathToSuggestion(p, replaceRange)); result.items = [...suggestions, ...result.items]; } + }; } -export function providePathSuggestions(value: string, range: Range, activeDocFsPath: string, root?: string): CompletionItem[] { - if (startsWith(value, '/') && !root) { +function stripQuotes(fullValue: string) { + if (startsWith(fullValue, `'`) || startsWith(fullValue, `"`)) { + return fullValue.slice(1, -1); + } else { + return fullValue; + } +} + +/** + * Get a list of path suggestions. Folder suggestions are suffixed with a slash. + */ +function providePaths(valueBeforeCursor: string, activeDocFsPath: string, root?: string): string[] { + if (startsWith(valueBeforeCursor, '/') && !root) { return []; } - let replaceRange: Range; - const lastIndexOfSlash = value.lastIndexOf('/'); - if (lastIndexOfSlash === -1) { - replaceRange = getFullReplaceRange(range); - } else { - const valueAfterLastSlash = value.slice(lastIndexOfSlash + 1); - replaceRange = getReplaceRange(range, valueAfterLastSlash); - } + const lastIndexOfSlash = valueBeforeCursor.lastIndexOf('/'); + const valueBeforeLastSlash = valueBeforeCursor.slice(0, lastIndexOfSlash + 1); - let parentDir: string; - if (lastIndexOfSlash === -1) { - parentDir = path.resolve(root); - } else { - const valueBeforeLastSlash = value.slice(0, lastIndexOfSlash + 1); - - parentDir = startsWith(value, '/') - ? path.resolve(root, '.' + valueBeforeLastSlash) - : path.resolve(activeDocFsPath, '..', valueBeforeLastSlash); - } + const parentDir = startsWith(valueBeforeCursor, '/') + ? path.resolve(root, '.' + valueBeforeLastSlash) + : path.resolve(activeDocFsPath, '..', valueBeforeLastSlash); try { return fs.readdirSync(parentDir).map(f => { - if (isDir(path.resolve(parentDir, f))) { - return { - label: f + '/', - kind: CompletionItemKind.Folder, - textEdit: TextEdit.replace(replaceRange, f + '/'), - command: { - title: 'Suggest', - command: 'editor.action.triggerSuggest' - } - }; - } else { - return { - label: f, - kind: CompletionItemKind.File, - textEdit: TextEdit.replace(replaceRange, f) - }; - } + return isDir(path.resolve(parentDir, f)) + ? f + '/' + : f; }); } catch (e) { return []; @@ -91,9 +82,64 @@ export function providePathSuggestions(value: string, range: Range, activeDocFsP } const isDir = (p: string) => { - return fs.statSync(p).isDirectory(); + try { + return fs.statSync(p).isDirectory(); + } catch (e) { + return false; + } }; +function pathToReplaceRange(valueBeforeCursor: string, fullValue: string, fullValueRange: Range) { + let replaceRange: Range; + const lastIndexOfSlash = valueBeforeCursor.lastIndexOf('/'); + if (lastIndexOfSlash === -1) { + replaceRange = fullValueRange; + } else { + // For cases where cursor is in the middle of attribute value, like ', { items: [ { label: 'location', resultText: '' }, @@ -166,6 +166,7 @@ suite('HTML Path Completion', () => { }); test('Empty Path Value', () => { + // document: index.html testCompletionFor(' + + \ No newline at end of file diff --git a/src/vs/code/electron-browser/processExplorer/processExplorer.js b/src/vs/code/electron-browser/processExplorer/processExplorer.js new file mode 100644 index 00000000000..1fdb4bb8296 --- /dev/null +++ b/src/vs/code/electron-browser/processExplorer/processExplorer.js @@ -0,0 +1,175 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +const path = require('path'); +const fs = require('fs'); +const remote = require('electron').remote; + +function assign(destination, source) { + return Object.keys(source) + .reduce(function (r, key) { r[key] = source[key]; return r; }, destination); +} + +function parseURLQueryArgs() { + const search = window.location.search || ''; + + return search.split(/[?&]/) + .filter(function (param) { return !!param; }) + .map(function (param) { return param.split('='); }) + .filter(function (param) { return param.length === 2; }) + .reduce(function (r, param) { r[param[0]] = decodeURIComponent(param[1]); return r; }, {}); +} + +function uriFromPath(_path) { + var pathName = path.resolve(_path).replace(/\\/g, '/'); + if (pathName.length > 0 && pathName.charAt(0) !== '/') { + pathName = '/' + pathName; + } + + return encodeURI('file://' + pathName); +} + +function readFile(file) { + return new Promise(function(resolve, reject) { + fs.readFile(file, 'utf8', function(err, data) { + if (err) { + reject(err); + return; + } + resolve(data); + }); + }); +} + +function main() { + const args = parseURLQueryArgs(); + const configuration = JSON.parse(args['config'] || '{}') || {}; + + assign(process.env, configuration.userEnv); + + //#region Add support for using node_modules.asar + (function () { + const path = require('path'); + const Module = require('module'); + let NODE_MODULES_PATH = path.join(configuration.appRoot, 'node_modules'); + if (/[a-z]\:/.test(NODE_MODULES_PATH)) { + // Make drive letter uppercase + NODE_MODULES_PATH = NODE_MODULES_PATH.charAt(0).toUpperCase() + NODE_MODULES_PATH.substr(1); + } + const NODE_MODULES_ASAR_PATH = NODE_MODULES_PATH + '.asar'; + + const originalResolveLookupPaths = Module._resolveLookupPaths; + Module._resolveLookupPaths = function (request, parent, newReturn) { + const result = originalResolveLookupPaths(request, parent, newReturn); + + const paths = newReturn ? result : result[1]; + for (let i = 0, len = paths.length; i < len; i++) { + if (paths[i] === NODE_MODULES_PATH) { + paths.splice(i, 0, NODE_MODULES_ASAR_PATH); + break; + } + } + + return result; + }; + })(); + //#endregion + + // Get the nls configuration into the process.env as early as possible. + var nlsConfig = { availableLanguages: {} }; + const config = process.env['VSCODE_NLS_CONFIG']; + if (config) { + process.env['VSCODE_NLS_CONFIG'] = config; + try { + nlsConfig = JSON.parse(config); + } catch (e) { /*noop*/ } + } + + if (nlsConfig._resolvedLanguagePackCoreLocation) { + let bundles = Object.create(null); + nlsConfig.loadBundle = function(bundle, language, cb) { + let result = bundles[bundle]; + if (result) { + cb(undefined, result); + return; + } + let bundleFile = path.join(nlsConfig._resolvedLanguagePackCoreLocation, bundle.replace(/\//g, '!') + '.nls.json'); + readFile(bundleFile).then(function (content) { + let json = JSON.parse(content); + bundles[bundle] = json; + cb(undefined, json); + }) + .catch(cb); + }; + } + + var locale = nlsConfig.availableLanguages['*'] || 'en'; + if (locale === 'zh-tw') { + locale = 'zh-Hant'; + } else if (locale === 'zh-cn') { + locale = 'zh-Hans'; + } + + window.document.documentElement.setAttribute('lang', locale); + + const extractKey = function (e) { + return [ + e.ctrlKey ? 'ctrl-' : '', + e.metaKey ? 'meta-' : '', + e.altKey ? 'alt-' : '', + e.shiftKey ? 'shift-' : '', + e.keyCode + ].join(''); + }; + + const TOGGLE_DEV_TOOLS_KB = (process.platform === 'darwin' ? 'meta-alt-73' : 'ctrl-shift-73'); // mac: Cmd-Alt-I, rest: Ctrl-Shift-I + const RELOAD_KB = (process.platform === 'darwin' ? 'meta-82' : 'ctrl-82'); // mac: Cmd-R, rest: Ctrl-R + + window.addEventListener('keydown', function (e) { + const key = extractKey(e); + if (key === TOGGLE_DEV_TOOLS_KB) { + remote.getCurrentWebContents().toggleDevTools(); + } else if (key === RELOAD_KB) { + remote.getCurrentWindow().reload(); + } + }); + + // Load the loader + const loaderFilename = configuration.appRoot + '/out/vs/loader.js'; + const loaderSource = fs.readFileSync(loaderFilename); + require('vm').runInThisContext(loaderSource, { filename: loaderFilename }); + var define = global.define; + global.define = undefined; + + window.nodeRequire = require.__$__nodeRequire; + + define('fs', ['original-fs'], function (originalFS) { return originalFS; }); // replace the patched electron fs with the original node fs for all AMD code + + window.MonacoEnvironment = {}; + const rootUrl = uriFromPath(configuration.appRoot) + '/out'; + + require.config({ + baseUrl: rootUrl, + 'vs/nls': nlsConfig, + nodeCachedDataDir: configuration.nodeCachedDataDir, + nodeModules: [/*BUILD->INSERT_NODE_MODULES*/] + }); + + if (nlsConfig.pseudo) { + require(['vs/nls'], function (nlsPlugin) { + nlsPlugin.setPseudoTranslation(nlsConfig.pseudo); + }); + } + + require([ + 'vs/code/electron-browser/processExplorer/processExplorerMain' + ], function (processExplorer) { + processExplorer.startup(configuration.data); + }); +} + +main(); diff --git a/src/vs/code/electron-browser/processExplorer/processExplorerMain.ts b/src/vs/code/electron-browser/processExplorer/processExplorerMain.ts new file mode 100644 index 00000000000..605f48afab8 --- /dev/null +++ b/src/vs/code/electron-browser/processExplorer/processExplorerMain.ts @@ -0,0 +1,184 @@ +/*--------------------------------------------------------------------------------------------- + * 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/processExplorer'; +import { listProcesses, ProcessItem } from 'vs/base/node/ps'; +import { remote, webFrame } from 'electron'; +import { repeat } from 'vs/base/common/strings'; +import { totalmem } from 'os'; +import product from 'vs/platform/node/product'; +import { localize } from 'vs/nls'; +import { ProcessExplorerStyles, ProcessExplorerData } from 'vs/platform/issue/common/issue'; +import * as browser from 'vs/base/browser/browser'; +import * as platform from 'vs/base/common/platform'; + +let processList: any[]; + +function getProcessList(rootProcess: ProcessItem) { + const processes: any[] = []; + + if (rootProcess) { + getProcessItem(processes, rootProcess, 0); + } + + return processes; +} + +function getProcessItem(processes: any[], item: ProcessItem, indent: number): void { + const isRoot = (indent === 0); + + const MB = 1024 * 1024; + + // Format name with indent + const name = isRoot ? `${product.applicationName} main` : item.name; + const formattedName = isRoot ? name : `${repeat(' ', indent)} ${name}`; + const memory = process.platform === 'win32' ? item.mem : (totalmem() * (item.mem / 100)); + processes.push({ + cpu: Number(item.load.toFixed(0)), + memory: Number((memory / MB).toFixed(0)), + pid: Number((item.pid).toFixed(0)), + name, + formattedName, + cmd: item.cmd + }); + + // Recurse into children if any + if (Array.isArray(item.children)) { + item.children.forEach(child => getProcessItem(processes, child, indent + 1)); + } +} + +function getProcessIdWithHighestProperty(processList, propertyName: string) { + let max = 0; + let maxProcessId; + processList.forEach(process => { + if (process[propertyName] > max) { + max = process[propertyName]; + maxProcessId = process.pid; + } + }); + + return maxProcessId; +} + +function updateProcessInfo(processList): void { + const target = document.getElementById('process-list'); + const highestCPUProcess = getProcessIdWithHighestProperty(processList, 'cpu'); + const highestMemoryProcess = getProcessIdWithHighestProperty(processList, 'memory'); + + let tableHtml = ` + + ${localize('cpu', "CPU %")} + ${localize('memory', "Memory (MB)")} + ${localize('pid', "pid")} + ${localize('name', "Name")} + `; + + processList.forEach(p => { + const cpuClass = p.pid === highestCPUProcess ? 'highest' : ''; + const memoryClass = p.pid === highestMemoryProcess ? 'highest' : ''; + + tableHtml += ` + + ${p.cpu} + ${p.memory} + ${p.pid} + ${p.formattedName} + `; + }); + + target.innerHTML = `${tableHtml}
`; +} + +function applyStyles(styles: ProcessExplorerStyles): void { + const styleTag = document.createElement('style'); + const content: string[] = []; + + if (styles.hoverBackground) { + content.push(`tbody > tr:hover { background-color: ${styles.hoverBackground}; }`); + } + + if (styles.hoverForeground) { + content.push(`tbody > tr:hover{ color: ${styles.hoverForeground}; }`); + } + + if (styles.highlightForeground) { + content.push(`.highest { color: ${styles.highlightForeground}; }`); + } + + styleTag.innerHTML = content.join('\n'); + document.head.appendChild(styleTag); + document.body.style.color = styles.color; +} + +function applyZoom(zoomLevel: number): void { + webFrame.setZoomLevel(zoomLevel); + browser.setZoomFactor(webFrame.getZoomFactor()); + // See https://github.com/Microsoft/vscode/issues/26151 + // Cannot be trusted because the webFrame might take some time + // until it really applies the new zoom level + browser.setZoomLevel(webFrame.getZoomLevel(), /*isTrusted*/false); +} + +function showContextMenu(e) { + e.preventDefault(); + + const pid = parseInt(e.currentTarget.id); + if (pid && typeof pid === 'number') { + const menu = new remote.Menu(); + menu.append(new remote.MenuItem({ + label: localize('killProcess', "Kill Process"), + click() { + process.kill(pid, 'SIGTERM'); + } + }) + ); + + menu.append(new remote.MenuItem({ + label: localize('forceKillProcess', "Force Kill Process"), + click() { + process.kill(pid, 'SIGKILL'); + } + }) + ); + + menu.popup(remote.getCurrentWindow()); + } +} + +export function startup(data: ProcessExplorerData): void { + applyStyles(data.styles); + applyZoom(data.zoomLevel); + + setInterval(() => listProcesses(remote.process.pid).then(processes => { + processList = getProcessList(processes); + updateProcessInfo(processList); + + const tableRows = document.getElementsByTagName('tr'); + for (let i = 0; i < tableRows.length; i++) { + const tableRow = tableRows[i]; + tableRow.addEventListener('click', (e) => { + showContextMenu(e); + }); + } + }), 1200); + + + document.onkeydown = (e: KeyboardEvent) => { + const cmdOrCtrlKey = platform.isMacintosh ? e.metaKey : e.ctrlKey; + + // Cmd/Ctrl + zooms in + if (cmdOrCtrlKey && e.keyCode === 187) { + applyZoom(webFrame.getZoomLevel() + 1); + } + + // Cmd/Ctrl - zooms out + if (cmdOrCtrlKey && e.keyCode === 189) { + applyZoom(webFrame.getZoomLevel() - 1); + } + }; +} \ No newline at end of file diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index 6a255ddf114..2060846bfc5 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -5,7 +5,7 @@ 'use strict'; -import { app, ipcMain as ipc, BrowserWindow } from 'electron'; +import { app, ipcMain as ipc } from 'electron'; import * as platform from 'vs/base/common/platform'; import { WindowsManager } from 'vs/code/electron-main/windows'; import { IWindowsService, OpenContext, ActiveWindowManager } from 'vs/platform/windows/common/windows'; @@ -60,6 +60,9 @@ import { IssueService } from 'vs/platform/issue/electron-main/issueService'; import { LogLevelSetterChannel } from 'vs/platform/log/common/logIpc'; import { setUnexpectedErrorHandler } from 'vs/base/common/errors'; import { ElectronURLListener } from 'vs/platform/url/electron-main/electronUrlListener'; +import { serve as serveDriver } from 'vs/platform/driver/electron-main/driver'; +import { join } from 'path'; +import { exists, rename } from 'vs/base/node/pfs'; export class CodeApplication { @@ -127,7 +130,7 @@ export class CodeApplication { return srcUri.startsWith(URI.file(this.environmentService.appRoot.toLowerCase()).toString()); }; - app.on('web-contents-created', (_event: any, contents) => { + app.on('web-contents-created', (event: any, contents) => { contents.on('will-attach-webview', (event: Electron.Event, webPreferences, params) => { delete webPreferences.preload; webPreferences.nodeIntegration = false; @@ -182,15 +185,15 @@ export class CodeApplication { this.windowsMainService.openNewWindow(OpenContext.DESKTOP); //macOS native tab "+" button }); - ipc.on('vscode:exit', (_event: any, code: number) => { + ipc.on('vscode:exit', (event: any, code: number) => { this.logService.trace('IPC#vscode:exit', code); this.dispose(); this.lifecycleService.kill(code); }); - ipc.on('vscode:fetchShellEnv', (_event: any, windowId: number) => { - const { webContents } = BrowserWindow.fromId(windowId); + ipc.on('vscode:fetchShellEnv', event => { + const webContents = event.sender.webContents; getShellEnvironment().then(shellEnv => { if (!webContents.isDestroyed()) { webContents.send('vscode:acceptShellEnv', shellEnv); @@ -204,7 +207,7 @@ export class CodeApplication { }); }); - ipc.on('vscode:broadcast', (_event: any, windowId: number, broadcast: { channel: string; payload: any; }) => { + ipc.on('vscode:broadcast', (event: any, windowId: number, broadcast: { channel: string; payload: any; }) => { if (this.windowsMainService && broadcast.channel && !isUndefinedOrNull(broadcast.payload)) { this.logService.trace('IPC#vscode:broadcast', broadcast.channel, broadcast.payload); @@ -261,38 +264,55 @@ export class CodeApplication { this.logService.debug(`from: ${this.environmentService.appRoot}`); this.logService.debug('args:', this.environmentService.args); - // Make sure we associate the program with the app user model id - // This will help Windows to associate the running program with - // any shortcut that is pinned to the taskbar and prevent showing - // two icons in the taskbar for the same app. - if (platform.isWindows && product.win32AppUserModelId) { - app.setAppUserModelId(product.win32AppUserModelId); - } + // Handle local storage (TODO@Ben remove me after a while) + return this.handleLocalStorage().then(() => { - // Create Electron IPC Server - this.electronIpcServer = new ElectronIPCServer(); + // Make sure we associate the program with the app user model id + // This will help Windows to associate the running program with + // any shortcut that is pinned to the taskbar and prevent showing + // two icons in the taskbar for the same app. + if (platform.isWindows && product.win32AppUserModelId) { + app.setAppUserModelId(product.win32AppUserModelId); + } - // Resolve unique machine ID - this.logService.trace('Resolving machine identifier...'); - return this.resolveMachineId().then(machineId => { - this.logService.trace(`Resolved machine identifier: ${machineId}`); + // Create Electron IPC Server + this.electronIpcServer = new ElectronIPCServer(); - // Spawn shared process - this.sharedProcess = new SharedProcess(this.environmentService, this.lifecycleService, this.logService, machineId, this.userEnv); - this.sharedProcessClient = this.sharedProcess.whenReady().then(() => connect(this.environmentService.sharedIPCHandle, 'main')); + // Resolve unique machine ID + this.logService.trace('Resolving machine identifier...'); + return this.resolveMachineId().then(machineId => { + this.logService.trace(`Resolved machine identifier: ${machineId}`); - // Services - const appInstantiationService = this.initServices(machineId); + // Spawn shared process + this.sharedProcess = new SharedProcess(this.environmentService, this.lifecycleService, this.logService, machineId, this.userEnv); + this.sharedProcessClient = this.sharedProcess.whenReady().then(() => connect(this.environmentService.sharedIPCHandle, 'main')); - // Setup Auth Handler - const authHandler = appInstantiationService.createInstance(ProxyAuthHandler); - this.toDispose.push(authHandler); + // Services + const appInstantiationService = this.initServices(machineId); - // Open Windows - appInstantiationService.invokeFunction(accessor => this.openFirstWindow(accessor)); + let promise: TPromise = TPromise.as(null); - // Post Open Windows Tasks - appInstantiationService.invokeFunction(accessor => this.afterWindowOpen(accessor)); + // Create driver + if (this.environmentService.driverHandle) { + serveDriver(this.electronIpcServer, this.environmentService.driverHandle, appInstantiationService).then(server => { + this.logService.info('Driver started at:', this.environmentService.driverHandle); + this.toDispose.push(server); + }); + } + + return promise.then(() => { + + // Setup Auth Handler + const authHandler = appInstantiationService.createInstance(ProxyAuthHandler); + this.toDispose.push(authHandler); + + // Open Windows + appInstantiationService.invokeFunction(accessor => this.openFirstWindow(accessor)); + + // Post Open Windows Tasks + appInstantiationService.invokeFunction(accessor => this.afterWindowOpen(accessor)); + }); + }); }); } @@ -311,6 +331,23 @@ export class CodeApplication { }); } + private handleLocalStorage(): TPromise { + const localStorageFile = join(this.environmentService.userDataPath, 'Local Storage', 'file__0.localstorage'); + const localStorageJournalFile = join(this.environmentService.userDataPath, 'Local Storage', 'file__0.localstorage-journal'); + const localStorageBackupFile = join(this.environmentService.userDataPath, 'Local Storage', 'file__0.localstorage.vscbak'); + const localStorageJournalBackupFile = join(this.environmentService.userDataPath, 'Local Storage', 'file__0.localstorage-journal.vscbak'); + + // Electron 1.7.12: Restore storage + return exists(localStorageBackupFile).then(localStorageBackupFileExists => { + return exists(localStorageJournalBackupFile).then(localStorageJournalBackupFileExists => { + return TPromise.join([ + localStorageBackupFileExists ? rename(localStorageBackupFile, localStorageFile) : TPromise.as(void 0), + localStorageJournalBackupFileExists ? rename(localStorageJournalBackupFile, localStorageJournalFile) : TPromise.as(void 0) + ]); + }); + }).then(() => void 0, () => void 0); + } + private initServices(machineId: string): IInstantiationService { const services = new ServiceCollection(); @@ -325,7 +362,7 @@ export class CodeApplication { services.set(IWindowsMainService, new SyncDescriptor(WindowsManager, machineId)); services.set(IWindowsService, new SyncDescriptor(WindowsService, this.sharedProcess)); services.set(ILaunchService, new SyncDescriptor(LaunchService)); - services.set(IIssueService, new SyncDescriptor(IssueService, machineId)); + services.set(IIssueService, new SyncDescriptor(IssueService, machineId, this.userEnv)); // Telemtry if (this.environmentService.isBuilt && !this.environmentService.isExtensionDevelopment && !this.environmentService.args['disable-telemetry'] && !!product.enableTelemetry) { @@ -402,11 +439,12 @@ export class CodeApplication { this.windowsMainService.ready(this.userEnv); // Open our first window + const macOpenFiles = (global).macOpenFiles as string[]; const context = !!process.env['VSCODE_CLI'] ? OpenContext.CLI : OpenContext.DESKTOP; if (args['new-window'] && args._.length === 0) { this.windowsMainService.open({ context, cli: args, forceNewWindow: true, forceEmpty: true, initialStartup: true }); // new window if "-n" was used without paths - } else if (global.macOpenFiles && global.macOpenFiles.length && (!args._ || !args._.length)) { - this.windowsMainService.open({ context: OpenContext.DOCK, cli: args, pathsToOpen: global.macOpenFiles, initialStartup: true }); // mac: open-file event received on startup + } else if (macOpenFiles && macOpenFiles.length && (!args._ || !args._.length)) { + this.windowsMainService.open({ context: OpenContext.DOCK, cli: args, pathsToOpen: macOpenFiles, initialStartup: true }); // mac: open-file event received on startup } else { this.windowsMainService.open({ context, cli: args, forceNewWindow: args['new-window'] || (!args._.length && args['unity-launch']), diffMode: args.diff, initialStartup: true }); // default: read paths from cli } diff --git a/src/vs/code/electron-main/diagnostics.ts b/src/vs/code/electron-main/diagnostics.ts index 8de2d49423c..c2d5a003f9f 100644 --- a/src/vs/code/electron-main/diagnostics.ts +++ b/src/vs/code/electron-main/diagnostics.ts @@ -29,6 +29,7 @@ export interface SystemInfo { VM: string; 'Screen Reader': string; 'Process Argv': string; + 'GPU Status': Electron.GPUFeatureStatus; } export interface ProcessInfo { @@ -92,7 +93,8 @@ export function getSystemInfo(info: IMainProcessInfo): SystemInfo { 'Memory (System)': `${(os.totalmem() / GB).toFixed(2)}GB (${(os.freemem() / GB).toFixed(2)}GB free)`, VM: `${Math.round((virtualMachineHint.value() * 100))}%`, 'Screen Reader': `${app.isAccessibilitySupportEnabled() ? 'yes' : 'no'}`, - 'Process Argv': `${info.mainArguments.join(' ')}` + 'Process Argv': `${info.mainArguments.join(' ')}`, + 'GPU Status': app.getGPUFeatureStatus() }; const cpus = os.cpus(); @@ -208,7 +210,14 @@ function formatLaunchConfigs(configs: WorkspaceStatItem[]): string { return output.join('\n'); } -function formatEnvironment(info: IMainProcessInfo): string { +function expandGPUFeatures(): string { + const gpuFeatures = app.getGPUFeatureStatus(); + const longestFeatureName = Math.max(...Object.keys(gpuFeatures).map(feature => feature.length)); + // Make columns aligned by adding spaces after feature name + return Object.keys(gpuFeatures).map(feature => `${feature}: ${repeat(' ', longestFeatureName - feature.length)} ${gpuFeatures[feature]}`).join('\n '); +} + +export function formatEnvironment(info: IMainProcessInfo): string { const MB = 1024 * 1024; const GB = 1024 * MB; @@ -226,6 +235,7 @@ function formatEnvironment(info: IMainProcessInfo): string { output.push(`VM: ${Math.round((virtualMachineHint.value() * 100))}%`); output.push(`Screen Reader: ${app.isAccessibilitySupportEnabled() ? 'yes' : 'no'}`); output.push(`Process Argv: ${info.mainArguments.join(' ')}`); + output.push(`GPU Status: ${expandGPUFeatures()}`); return output.join('\n'); } diff --git a/src/vs/code/electron-main/menus.ts b/src/vs/code/electron-main/menus.ts index 442e70d33c9..8cc7ad73dd1 100644 --- a/src/vs/code/electron-main/menus.ts +++ b/src/vs/code/electron-main/menus.ts @@ -100,7 +100,7 @@ export class CodeMenu { this.windowsMainService.onWindowClose(() => this.updateWorkspaceMenuItems()); // Listen to extension viewlets - ipc.on('vscode:extensionViewlets', (_event: any, rawExtensionViewlets: string) => { + ipc.on('vscode:extensionViewlets', (event: any, rawExtensionViewlets: string) => { let extensionViewlets: IExtensionViewlet[] = []; try { extensionViewlets = JSON.parse(rawExtensionViewlets); @@ -451,7 +451,7 @@ export class CodeMenu { } private getPreferencesMenu(): Electron.MenuItem { - const settings = this.createMenuItem(nls.localize({ key: 'miOpenSettings', comment: ['&& denotes a mnemonic'] }, "&&Settings"), 'workbench.action.openGlobalSettings'); + const settings = this.createMenuItem(nls.localize({ key: 'miOpenSettings', comment: ['&& denotes a mnemonic'] }, "&&Settings"), 'workbench.action.openSettings'); const kebindingSettings = this.createMenuItem(nls.localize({ key: 'miOpenKeymap', comment: ['&& denotes a mnemonic'] }, "&&Keyboard Shortcuts"), 'workbench.action.openGlobalKeybindings'); const keymapExtensions = this.createMenuItem(nls.localize({ key: 'miOpenKeymapExtensions', comment: ['&& denotes a mnemonic'] }, "&&Keymap Extensions"), 'workbench.extensions.action.showRecommendedKeymapExtensions'); const snippetsSettings = this.createMenuItem(nls.localize({ key: 'miOpenSnippets', comment: ['&& denotes a mnemonic'] }, "User &&Snippets"), 'workbench.action.openSnippets'); @@ -854,6 +854,7 @@ export class CodeMenu { breakpointsMenu.append(this.createMenuItem(nls.localize({ key: 'miConditionalBreakpoint', comment: ['&& denotes a mnemonic'] }, "&&Conditional Breakpoint..."), 'editor.debug.action.conditionalBreakpoint')); breakpointsMenu.append(this.createMenuItem(nls.localize({ key: 'miColumnBreakpoint', comment: ['&& denotes a mnemonic'] }, "C&&olumn Breakpoint"), 'editor.debug.action.toggleColumnBreakpoint')); breakpointsMenu.append(this.createMenuItem(nls.localize({ key: 'miFunctionBreakpoint', comment: ['&& denotes a mnemonic'] }, "&&Function Breakpoint..."), 'workbench.debug.viewlet.action.addFunctionBreakpointAction')); + breakpointsMenu.append(this.createMenuItem(nls.localize({ key: 'miLogPoint', comment: ['&& denotes a mnemonic'] }, "&&Log Point..."), 'editor.debug.action.toggleLogPoint')); const newBreakpoints = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'miNewBreakpoint', comment: ['&& denotes a mnemonic'] }, "&&New Breakpoint")), submenu: breakpointsMenu }); const enableAllBreakpoints = this.createMenuItem(nls.localize({ key: 'miEnableAllBreakpoints', comment: ['&& denotes a mnemonic'] }, "Enable All Breakpoints"), 'workbench.debug.viewlet.action.enableAllBreakpoints'); const disableAllBreakpoints = this.createMenuItem(nls.localize({ key: 'miDisableAllBreakpoints', comment: ['&& denotes a mnemonic'] }, "Disable A&&ll Breakpoints"), 'workbench.debug.viewlet.action.disableAllBreakpoints'); @@ -942,6 +943,8 @@ export class CodeMenu { } }, false)); + const openProcessExplorer = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'miOpenProcessExplorerer', comment: ['&& denotes a mnemonic'] }, "Open &&Process Explorer")), click: () => this.runActionInRenderer('workbench.action.openProcessExplorer') }); + let reportIssuesItem: Electron.MenuItem = null; if (product.reportIssueUrl) { const label = nls.localize({ key: 'miReportIssue', comment: ['&& denotes a mnemonic', 'Translate this to "Report Issue in English" in all languages please!'] }, "Report &&Issue"); @@ -990,7 +993,8 @@ export class CodeMenu { }) : null, (product.licenseUrl || product.privacyStatementUrl) ? __separator__() : null, toggleDevToolsItem, - isWindows && product.quality !== 'stable' ? showAccessibilityOptions : null + openProcessExplorer, + isWindows && product.quality !== 'stable' ? showAccessibilityOptions : null, ]).forEach(item => helpMenu.append(item)); if (!isMacintosh) { @@ -1029,7 +1033,7 @@ export class CodeMenu { } private openAccessibilityOptions(): void { - let win = new BrowserWindow({ + const win = new BrowserWindow({ alwaysOnTop: true, skipTaskbar: true, resizable: false, diff --git a/src/vs/code/electron-main/window.ts b/src/vs/code/electron-main/window.ts index dbd4821bea1..af71a291e7b 100644 --- a/src/vs/code/electron-main/window.ts +++ b/src/vs/code/electron-main/window.ts @@ -151,8 +151,16 @@ export class CodeWindow implements ICodeWindow { const windowConfig = this.configurationService.getValue('window'); + if (isMacintosh) { + options.acceptFirstMouse = true; // enabled by default + + if (windowConfig && windowConfig.clickThroughInactive === false) { + options.acceptFirstMouse = false; + } + } + let useNativeTabs = false; - if (windowConfig && windowConfig.nativeTabs) { + if (isMacintosh && windowConfig && windowConfig.nativeTabs === true) { options.tabbingIdentifier = product.nameShort; // this opts in to sierra tabs useNativeTabs = true; } @@ -598,7 +606,7 @@ export class CodeWindow implements ICodeWindow { // Perf Counters windowConfiguration.perfEntries = exportEntries(); - windowConfiguration.perfStartTime = global.perfStartTime; + windowConfiguration.perfStartTime = (global).perfStartTime; windowConfiguration.perfWindowLoadTime = Date.now(); // Config (combination of process.argv and window configuration) diff --git a/src/vs/code/electron-main/windows.ts b/src/vs/code/electron-main/windows.ts index 85f39a00a01..8f15684a41f 100644 --- a/src/vs/code/electron-main/windows.ts +++ b/src/vs/code/electron-main/windows.ts @@ -169,7 +169,7 @@ export class WindowsManager implements IWindowsMainService { }); // React to workbench loaded events from windows - ipc.on('vscode:workbenchLoaded', (_event: any, windowId: number) => { + ipc.on('vscode:workbenchLoaded', (event: any, windowId: number) => { this.logService.trace('IPC#vscode-workbenchLoaded'); const win = this.getWindowById(windowId); diff --git a/src/vs/editor/browser/codeEditor.ts b/src/vs/editor/browser/codeEditor.ts index 1c5ed6d9b6b..160eac3591e 100644 --- a/src/vs/editor/browser/codeEditor.ts +++ b/src/vs/editor/browser/codeEditor.ts @@ -12,6 +12,7 @@ import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; import { EditorAction, EditorExtensionsRegistry, IEditorContributionCtor } from 'vs/editor/browser/editorExtensions'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { INotificationService } from 'vs/platform/notification/common/notification'; export class CodeEditor extends CodeEditorWidget { @@ -22,9 +23,10 @@ export class CodeEditor extends CodeEditorWidget { @ICodeEditorService codeEditorService: ICodeEditorService, @ICommandService commandService: ICommandService, @IContextKeyService contextKeyService: IContextKeyService, - @IThemeService themeService: IThemeService + @IThemeService themeService: IThemeService, + @INotificationService notificationService: INotificationService ) { - super(domElement, options, false, instantiationService, codeEditorService, commandService, contextKeyService, themeService); + super(domElement, options, false, instantiationService, codeEditorService, commandService, contextKeyService, themeService, notificationService); } protected _getContributions(): IEditorContributionCtor[] { diff --git a/src/vs/editor/browser/config/configuration.ts b/src/vs/editor/browser/config/configuration.ts index d89a9095a7b..74cb598bddf 100644 --- a/src/vs/editor/browser/config/configuration.ts +++ b/src/vs/editor/browser/config/configuration.ts @@ -276,8 +276,21 @@ class CSSBasedConfiguration extends Disposable { export class Configuration extends CommonEditorConfiguration { + private static _massageFontFamily(fontFamily: string): string { + if (/[,"']/.test(fontFamily)) { + // Looks like the font family might be already escaped + return fontFamily; + } + if (/[+ ]/.test(fontFamily)) { + // Wrap a font family using + or with quotes + return `"${fontFamily}"`; + } + + return fontFamily; + } + public static applyFontInfoSlow(domNode: HTMLElement, fontInfo: BareFontInfo): void { - domNode.style.fontFamily = fontInfo.fontFamily; + domNode.style.fontFamily = Configuration._massageFontFamily(fontInfo.fontFamily); domNode.style.fontWeight = fontInfo.fontWeight; domNode.style.fontSize = fontInfo.fontSize + 'px'; domNode.style.lineHeight = fontInfo.lineHeight + 'px'; @@ -285,7 +298,7 @@ export class Configuration extends CommonEditorConfiguration { } public static applyFontInfo(domNode: FastDomNode, fontInfo: BareFontInfo): void { - domNode.setFontFamily(fontInfo.fontFamily); + domNode.setFontFamily(Configuration._massageFontFamily(fontInfo.fontFamily)); domNode.setFontWeight(fontInfo.fontWeight); domNode.setFontSize(fontInfo.fontSize); domNode.setLineHeight(fontInfo.lineHeight); @@ -349,7 +362,7 @@ export class Configuration extends CommonEditorConfiguration { extraEditorClassName: this._getExtraEditorClassName(), outerWidth: this._elementSizeObserver.getWidth(), outerHeight: this._elementSizeObserver.getHeight(), - emptySelectionClipboard: browser.isWebKit, + emptySelectionClipboard: browser.isWebKit || browser.isFirefox, pixelRatio: browser.getPixelRatio(), zoomLevel: browser.getZoomLevel(), accessibilitySupport: browser.getAccessibilitySupport() diff --git a/src/vs/editor/browser/controller/mouseHandler.ts b/src/vs/editor/browser/controller/mouseHandler.ts index efe3c993f16..6657158e914 100644 --- a/src/vs/editor/browser/controller/mouseHandler.ts +++ b/src/vs/editor/browser/controller/mouseHandler.ts @@ -11,7 +11,7 @@ import * as dom from 'vs/base/browser/dom'; import { Position } from 'vs/editor/common/core/position'; import { Selection } from 'vs/editor/common/core/selection'; import { ViewEventHandler } from 'vs/editor/common/viewModel/viewEventHandler'; -import { MouseTarget, MouseTargetFactory, IViewZoneData } from 'vs/editor/browser/controller/mouseTarget'; +import { MouseTarget, MouseTargetFactory, IViewZoneData, HitTestContext } from 'vs/editor/browser/controller/mouseTarget'; import * as editorBrowser from 'vs/editor/browser/editorBrowser'; import { TimeoutTimer, RunOnceScheduler } from 'vs/base/common/async'; import { ViewContext } from 'vs/editor/common/view/viewContext'; @@ -220,8 +220,8 @@ export class MouseHandler extends ViewEventHandler { let targetIsViewZone = (t.type === editorBrowser.MouseTargetType.CONTENT_VIEW_ZONE || t.type === editorBrowser.MouseTargetType.GUTTER_VIEW_ZONE); let targetIsWidget = (t.type === editorBrowser.MouseTargetType.CONTENT_WIDGET); - let shouldHandle = e.leftButton; - if (platform.isMacintosh && e.ctrlKey) { + let shouldHandle = e.leftButton || e.middleButton; + if (platform.isMacintosh && e.leftButton && e.ctrlKey) { shouldHandle = false; } @@ -334,6 +334,7 @@ class MouseDownOperation extends Disposable { this._lastMouseEvent = e; this._mouseState.setStartedOnLineNumbers(targetType === editorBrowser.MouseTargetType.GUTTER_LINE_NUMBERS); + this._mouseState.setStartButtons(e); this._mouseState.setModifiers(e); let position = this._findMousePosition(e, true); if (!position) { @@ -423,12 +424,30 @@ class MouseDownOperation extends Disposable { const mouseColumn = this._getMouseColumn(e); if (e.posy < editorContent.y) { - let aboveLineNumber = viewLayout.getLineNumberAtVerticalOffset(Math.max(viewLayout.getCurrentScrollTop() - (editorContent.y - e.posy), 0)); + const verticalOffset = Math.max(viewLayout.getCurrentScrollTop() - (editorContent.y - e.posy), 0); + const viewZoneData = HitTestContext.getZoneAtCoord(this._context, verticalOffset); + if (viewZoneData) { + const newPosition = this._helpPositionJumpOverViewZone(viewZoneData); + if (newPosition) { + return new MouseTarget(null, editorBrowser.MouseTargetType.OUTSIDE_EDITOR, mouseColumn, newPosition); + } + } + + let aboveLineNumber = viewLayout.getLineNumberAtVerticalOffset(verticalOffset); return new MouseTarget(null, editorBrowser.MouseTargetType.OUTSIDE_EDITOR, mouseColumn, new Position(aboveLineNumber, 1)); } if (e.posy > editorContent.y + editorContent.height) { - let belowLineNumber = viewLayout.getLineNumberAtVerticalOffset(viewLayout.getCurrentScrollTop() + (e.posy - editorContent.y)); + const verticalOffset = viewLayout.getCurrentScrollTop() + (e.posy - editorContent.y); + const viewZoneData = HitTestContext.getZoneAtCoord(this._context, verticalOffset); + if (viewZoneData) { + const newPosition = this._helpPositionJumpOverViewZone(viewZoneData); + if (newPosition) { + return new MouseTarget(null, editorBrowser.MouseTargetType.OUTSIDE_EDITOR, mouseColumn, newPosition); + } + } + + let belowLineNumber = viewLayout.getLineNumberAtVerticalOffset(verticalOffset); return new MouseTarget(null, editorBrowser.MouseTargetType.OUTSIDE_EDITOR, mouseColumn, new Position(belowLineNumber, model.getLineMaxColumn(belowLineNumber))); } @@ -458,24 +477,31 @@ class MouseDownOperation extends Disposable { } if (t.type === editorBrowser.MouseTargetType.CONTENT_VIEW_ZONE || t.type === editorBrowser.MouseTargetType.GUTTER_VIEW_ZONE) { - // Force position on view zones to go above or below depending on where selection started from - let selectionStart = new Position(this._currentSelection.selectionStartLineNumber, this._currentSelection.selectionStartColumn); - let viewZoneData = t.detail; - let positionBefore = viewZoneData.positionBefore; - let positionAfter = viewZoneData.positionAfter; - - if (positionBefore && positionAfter) { - if (positionBefore.isBefore(selectionStart)) { - return new MouseTarget(t.element, t.type, t.mouseColumn, positionBefore, null, t.detail); - } else { - return new MouseTarget(t.element, t.type, t.mouseColumn, positionAfter, null, t.detail); - } + const newPosition = this._helpPositionJumpOverViewZone(t.detail); + if (newPosition) { + return new MouseTarget(t.element, t.type, t.mouseColumn, newPosition, null, t.detail); } } return t; } + private _helpPositionJumpOverViewZone(viewZoneData: IViewZoneData): Position { + // Force position on view zones to go above or below depending on where selection started from + let selectionStart = new Position(this._currentSelection.selectionStartLineNumber, this._currentSelection.selectionStartColumn); + let positionBefore = viewZoneData.positionBefore; + let positionAfter = viewZoneData.positionAfter; + + if (positionBefore && positionAfter) { + if (positionBefore.isBefore(selectionStart)) { + return positionBefore; + } else { + return positionAfter; + } + } + return null; + } + private _dispatchMouse(position: MouseTarget, inSelectionMode: boolean): void { this._viewController.dispatchMouse({ position: position.position, @@ -488,6 +514,9 @@ class MouseDownOperation extends Disposable { ctrlKey: this._mouseState.ctrlKey, metaKey: this._mouseState.metaKey, shiftKey: this._mouseState.shiftKey, + + leftButton: this._mouseState.leftButton, + middleButton: this._mouseState.middleButton, }); } } @@ -508,6 +537,12 @@ class MouseDownState { private _shiftKey: boolean; public get shiftKey(): boolean { return this._shiftKey; } + private _leftButton: boolean; + public get leftButton(): boolean { return this._leftButton; } + + private _middleButton: boolean; + public get middleButton(): boolean { return this._middleButton; } + private _startedOnLineNumbers: boolean; public get startedOnLineNumbers(): boolean { return this._startedOnLineNumbers; } @@ -522,6 +557,8 @@ class MouseDownState { this._ctrlKey = false; this._metaKey = false; this._shiftKey = false; + this._leftButton = false; + this._middleButton = false; this._startedOnLineNumbers = false; this._lastMouseDownPosition = null; this._lastMouseDownPositionEqualCount = 0; @@ -541,6 +578,11 @@ class MouseDownState { this._shiftKey = source.shiftKey; } + public setStartButtons(source: EditorMouseEvent) { + this._leftButton = source.leftButton; + this._middleButton = source.middleButton; + } + public setStartedOnLineNumbers(startedOnLineNumbers: boolean): void { this._startedOnLineNumbers = startedOnLineNumbers; } diff --git a/src/vs/editor/browser/controller/mouseTarget.ts b/src/vs/editor/browser/controller/mouseTarget.ts index 436f7a96967..bab014e28eb 100644 --- a/src/vs/editor/browser/controller/mouseTarget.ts +++ b/src/vs/editor/browser/controller/mouseTarget.ts @@ -16,6 +16,7 @@ import { PartFingerprint, PartFingerprints } from 'vs/editor/browser/view/viewPa import { IViewModel } from 'vs/editor/common/viewModel/viewModel'; import { EditorLayoutInfo } from 'vs/editor/common/config/editorOptions'; import { ViewLine } from 'vs/editor/browser/viewParts/lines/viewLine'; +import { HorizontalRange } from 'vs/editor/common/view/renderingContext'; export interface IViewZoneData { viewZoneId: number; @@ -174,6 +175,14 @@ class ElementPath { ); } + public static isStrictChildOfViewLines(path: Uint8Array): boolean { + return ( + path.length > 4 + && path[0] === PartFingerprint.OverflowGuard + && path[3] === PartFingerprint.ViewLines + ); + } + public static isChildOfScrollableElement(path: Uint8Array): boolean { return ( path.length >= 2 @@ -214,7 +223,7 @@ class ElementPath { } } -class HitTestContext { +export class HitTestContext { public readonly model: IViewModel; public readonly layoutInfo: EditorLayoutInfo; @@ -238,12 +247,16 @@ class HitTestContext { } public getZoneAtCoord(mouseVerticalOffset: number): IViewZoneData { + return HitTestContext.getZoneAtCoord(this._context, mouseVerticalOffset); + } + + public static getZoneAtCoord(context: ViewContext, mouseVerticalOffset: number): IViewZoneData { // The target is either a view zone or the empty space after the last view-line - let viewZoneWhitespace = this._context.viewLayout.getWhitespaceAtVerticalOffset(mouseVerticalOffset); + let viewZoneWhitespace = context.viewLayout.getWhitespaceAtVerticalOffset(mouseVerticalOffset); if (viewZoneWhitespace) { let viewZoneMiddle = viewZoneWhitespace.verticalOffset + viewZoneWhitespace.height / 2, - lineCount = this._context.model.getLineCount(), + lineCount = context.model.getLineCount(), positionBefore: Position = null, position: Position, positionAfter: Position = null; @@ -254,7 +267,7 @@ class HitTestContext { } if (viewZoneWhitespace.afterLineNumber > 0) { // There are more lines above this view zone - positionBefore = new Position(viewZoneWhitespace.afterLineNumber, this._context.model.getLineMaxColumn(viewZoneWhitespace.afterLineNumber)); + positionBefore = new Position(viewZoneWhitespace.afterLineNumber, context.model.getLineMaxColumn(viewZoneWhitespace.afterLineNumber)); } if (positionAfter === null) { @@ -330,7 +343,7 @@ class HitTestContext { return this._viewHelper.getLineWidth(lineNumber); } - public visibleRangeForPosition2(lineNumber: number, column: number) { + public visibleRangeForPosition2(lineNumber: number, column: number): HorizontalRange { return this._viewHelper.visibleRangeForPosition2(lineNumber, column); } @@ -621,6 +634,15 @@ export class MouseTargetFactory { } if (domHitTestExecuted) { + // Check if we are hitting a view-line (can happen in the case of inline decorations on empty lines) + // See https://github.com/Microsoft/vscode/issues/46942 + if (ElementPath.isStrictChildOfViewLines(request.targetPath)) { + const lineNumber = ctx.getLineNumberAtVerticalOffset(request.mouseVerticalOffset); + if (ctx.model.getLineLength(lineNumber) === 0) { + return request.fulfill(MouseTargetType.CONTENT_EMPTY, new Position(lineNumber, 1), void 0, EMPTY_CONTENT_IN_LINES); + } + } + // We have already executed hit test... return request.fulfill(MouseTargetType.UNKNOWN); } diff --git a/src/vs/editor/browser/controller/textAreaHandler.ts b/src/vs/editor/browser/controller/textAreaHandler.ts index cf5620fdb7c..098af888c20 100644 --- a/src/vs/editor/browser/controller/textAreaHandler.ts +++ b/src/vs/editor/browser/controller/textAreaHandler.ts @@ -27,6 +27,7 @@ import { LineNumbersOverlay } from 'vs/editor/browser/viewParts/lineNumbers/line import { BareFontInfo } from 'vs/editor/common/config/fontInfo'; import { RenderLineNumbersType } from 'vs/editor/common/config/editorOptions'; import { EndOfLinePreference } from 'vs/editor/common/model'; +import { getMapForWordSeparators, WordCharacterClass } from 'vs/editor/common/controller/wordCharacterClassifier'; export interface ITextAreaHandlerHelper { visibleRangeForPositionRelativeToEditor(lineNumber: number, column: number): HorizontalRange; @@ -203,16 +204,19 @@ export class TextAreaHandler extends ViewPart { if (this._accessibilitySupport === platform.AccessibilitySupport.Disabled) { // We know for a fact that a screen reader is not attached // On OSX, we write the character before the cursor to allow for "long-press" composition + // Also on OSX, we write the word before the cursor to allow for the Accessibility Keyboard to give good hints if (platform.isMacintosh) { const selection = this._selections[0]; if (selection.isEmpty()) { const position = selection.getStartPosition(); - if (position.column > 1) { - const lineContent = this._context.model.getLineContent(position.lineNumber); - const charBefore = lineContent.charAt(position.column - 2); - if (!strings.isHighSurrogate(charBefore.charCodeAt(0))) { - return new TextAreaState(charBefore, 1, 1, position, position); - } + + let textBefore = this._getWordBeforePosition(position); + if (textBefore.length === 0) { + textBefore = this._getCharacterBeforePosition(position); + } + + if (textBefore.length > 0) { + return new TextAreaState(textBefore, textBefore.length, textBefore.length, position, position); } } } @@ -328,6 +332,35 @@ export class TextAreaHandler extends ViewPart { super.dispose(); } + private _getWordBeforePosition(position: Position): string { + const lineContent = this._context.model.getLineContent(position.lineNumber); + const wordSeparators = getMapForWordSeparators(this._context.configuration.editor.wordSeparators); + + let column = position.column; + let distance = 0; + while (column > 1) { + const charCode = lineContent.charCodeAt(column - 2); + const charClass = wordSeparators.get(charCode); + if (charClass !== WordCharacterClass.Regular || distance > 50) { + return lineContent.substring(column - 1, position.column - 1); + } + distance++; + column--; + } + return lineContent.substring(0, position.column - 1); + } + + private _getCharacterBeforePosition(position: Position): string { + if (position.column > 1) { + const lineContent = this._context.model.getLineContent(position.lineNumber); + const charBefore = lineContent.charAt(position.column - 2); + if (!strings.isHighSurrogate(charBefore.charCodeAt(0))) { + return charBefore; + } + } + return ''; + } + // --- begin event handlers public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean { @@ -509,7 +542,7 @@ export class TextAreaHandler extends ViewPart { tac.setHeight(1); if (this._context.configuration.editor.viewInfo.glyphMargin) { - tac.setClassName('monaco-editor-background textAreaCover ' + Margin.CLASS_NAME); + tac.setClassName('monaco-editor-background textAreaCover ' + Margin.OUTER_CLASS_NAME); } else { if (this._context.configuration.editor.viewInfo.renderLineNumbers !== RenderLineNumbersType.Off) { tac.setClassName('monaco-editor-background textAreaCover ' + LineNumbersOverlay.CLASS_NAME); diff --git a/src/vs/editor/browser/controller/textAreaInput.ts b/src/vs/editor/browser/controller/textAreaInput.ts index 054dbaa48e4..d3dd81fac4e 100644 --- a/src/vs/editor/browser/controller/textAreaInput.ts +++ b/src/vs/editor/browser/controller/textAreaInput.ts @@ -42,6 +42,19 @@ export interface ITextAreaInputHost { deduceModelPosition(viewAnchorPosition: Position, deltaOffset: number, lineFeedCnt: number): Position; } +const enum TextAreaInputEventType { + none, + compositionstart, + compositionupdate, + compositionend, + input, + cut, + copy, + paste, + focus, + blur +} + /** * Writes screen reader content to the textarea and is able to analyze its input events to generate: * - onCut @@ -89,6 +102,7 @@ export class TextAreaInput extends Disposable { private readonly _host: ITextAreaInputHost; private readonly _textArea: TextAreaWrapper; + private _lastTextAreaEvent: TextAreaInputEventType; private readonly _asyncTriggerCut: RunOnceScheduler; private _textAreaState: TextAreaState; @@ -101,6 +115,7 @@ export class TextAreaInput extends Disposable { super(); this._host = host; this._textArea = this._register(new TextAreaWrapper(textArea)); + this._lastTextAreaEvent = TextAreaInputEventType.none; this._asyncTriggerCut = this._register(new RunOnceScheduler(() => this._onCut.fire(), 0)); this._textAreaState = TextAreaState.EMPTY; @@ -129,6 +144,8 @@ export class TextAreaInput extends Disposable { })); this._register(dom.addDisposableListener(textArea.domNode, 'compositionstart', (e: CompositionEvent) => { + this._lastTextAreaEvent = TextAreaInputEventType.compositionstart; + if (this._isDoingComposition) { return; } @@ -145,10 +162,10 @@ export class TextAreaInput extends Disposable { /** * Deduce the typed input from a text area's value and the last observed state. */ - const deduceInputFromTextAreaValue = (couldBeEmojiInput: boolean): [TextAreaState, ITypeData] => { + const deduceInputFromTextAreaValue = (couldBeEmojiInput: boolean, couldBeTypingAtOffset0: boolean): [TextAreaState, ITypeData] => { const oldState = this._textAreaState; - const newState = this._textAreaState.readFromTextArea(this._textArea); - return [newState, TextAreaState.deduceInput(oldState, newState, couldBeEmojiInput)]; + const newState = TextAreaState.readFromTextArea(this._textArea); + return [newState, TextAreaState.deduceInput(oldState, newState, couldBeEmojiInput, couldBeTypingAtOffset0)]; }; /** @@ -185,6 +202,8 @@ export class TextAreaInput extends Disposable { }; this._register(dom.addDisposableListener(textArea.domNode, 'compositionupdate', (e: CompositionEvent) => { + this._lastTextAreaEvent = TextAreaInputEventType.compositionupdate; + if (browser.isChromev56) { // See https://github.com/Microsoft/monaco-editor/issues/320 // where compositionupdate .data is broken in Chrome v55 and v56 @@ -195,7 +214,7 @@ export class TextAreaInput extends Disposable { } if (compositionDataInValid(e.locale)) { - const [newState, typeInput] = deduceInputFromTextAreaValue(/*couldBeEmojiInput*/false); + const [newState, typeInput] = deduceInputFromTextAreaValue(/*couldBeEmojiInput*/false, /*couldBeTypingAtOffset0*/false); this._textAreaState = newState; this._onType.fire(typeInput); this._onCompositionUpdate.fire(e); @@ -209,9 +228,11 @@ export class TextAreaInput extends Disposable { })); this._register(dom.addDisposableListener(textArea.domNode, 'compositionend', (e: CompositionEvent) => { + this._lastTextAreaEvent = TextAreaInputEventType.compositionend; + if (compositionDataInValid(e.locale)) { // https://github.com/Microsoft/monaco-editor/issues/339 - const [newState, typeInput] = deduceInputFromTextAreaValue(/*couldBeEmojiInput*/false); + const [newState, typeInput] = deduceInputFromTextAreaValue(/*couldBeEmojiInput*/false, /*couldBeTypingAtOffset0*/false); this._textAreaState = newState; this._onType.fire(typeInput); } else { @@ -223,7 +244,7 @@ export class TextAreaInput extends Disposable { // Due to isEdgeOrIE (where the textarea was not cleared initially) and isChrome (the textarea is not updated correctly when composition ends) // we cannot assume the text at the end consists only of the composited text if (browser.isEdgeOrIE || browser.isChrome) { - this._textAreaState = this._textAreaState.readFromTextArea(this._textArea); + this._textAreaState = TextAreaState.readFromTextArea(this._textArea); } if (!this._isDoingComposition) { @@ -235,6 +256,10 @@ export class TextAreaInput extends Disposable { })); this._register(dom.addDisposableListener(textArea.domNode, 'input', () => { + // We want to find out if this is the first `input` after a `focus`. + const previousEventWasFocus = (this._lastTextAreaEvent === TextAreaInputEventType.focus); + this._lastTextAreaEvent = TextAreaInputEventType.input; + // Pretend here we touched the text area, as the `input` event will most likely // result in a `selectionchange` event which we want to ignore this._textArea.setIgnoreSelectionChangeTime('received input event'); @@ -254,7 +279,7 @@ export class TextAreaInput extends Disposable { return; } - const [newState, typeInput] = deduceInputFromTextAreaValue(/*couldBeEmojiInput*/platform.isMacintosh); + const [newState, typeInput] = deduceInputFromTextAreaValue(/*couldBeEmojiInput*/platform.isMacintosh, /*couldBeTypingAtOffset0*/previousEventWasFocus && platform.isMacintosh); if (typeInput.replaceCharCnt === 0 && typeInput.text.length === 1 && strings.isHighSurrogate(typeInput.text.charCodeAt(0))) { // Ignore invalid input but keep it around for next time return; @@ -279,6 +304,8 @@ export class TextAreaInput extends Disposable { // --- Clipboard operations this._register(dom.addDisposableListener(textArea.domNode, 'cut', (e: ClipboardEvent) => { + this._lastTextAreaEvent = TextAreaInputEventType.cut; + // Pretend here we touched the text area, as the `cut` event will most likely // result in a `selectionchange` event which we want to ignore this._textArea.setIgnoreSelectionChangeTime('received cut event'); @@ -288,10 +315,14 @@ export class TextAreaInput extends Disposable { })); this._register(dom.addDisposableListener(textArea.domNode, 'copy', (e: ClipboardEvent) => { + this._lastTextAreaEvent = TextAreaInputEventType.copy; + this._ensureClipboardGetsEditorSelection(e); })); this._register(dom.addDisposableListener(textArea.domNode, 'paste', (e: ClipboardEvent) => { + this._lastTextAreaEvent = TextAreaInputEventType.paste; + // Pretend here we touched the text area, as the `paste` event will most likely // result in a `selectionchange` event which we want to ignore this._textArea.setIgnoreSelectionChangeTime('received paste event'); @@ -312,8 +343,14 @@ export class TextAreaInput extends Disposable { } })); - this._register(dom.addDisposableListener(textArea.domNode, 'focus', () => this._setHasFocus(true))); - this._register(dom.addDisposableListener(textArea.domNode, 'blur', () => this._setHasFocus(false))); + this._register(dom.addDisposableListener(textArea.domNode, 'focus', () => { + this._lastTextAreaEvent = TextAreaInputEventType.focus; + this._setHasFocus(true); + })); + this._register(dom.addDisposableListener(textArea.domNode, 'blur', () => { + this._lastTextAreaEvent = TextAreaInputEventType.blur; + this._setHasFocus(false); + })); // See https://github.com/Microsoft/vscode/issues/27216 @@ -419,12 +456,6 @@ export class TextAreaInput extends Disposable { } this._hasFocus = newHasFocus; - if (this._isDoingComposition) { - // textarea gets focus, so the state should be clean - // https://github.com/Microsoft/monaco-editor/issues/552 - this._isDoingComposition = false; - } - if (this._hasFocus) { if (browser.isEdge) { // Edge has a bug where setting the selection range while the focus event diff --git a/src/vs/editor/browser/controller/textAreaState.ts b/src/vs/editor/browser/controller/textAreaState.ts index c46ad756789..09f54a8a013 100644 --- a/src/vs/editor/browser/controller/textAreaState.ts +++ b/src/vs/editor/browser/controller/textAreaState.ts @@ -51,7 +51,7 @@ export class TextAreaState { return '[ <' + this.value + '>, selectionStart: ' + this.selectionStart + ', selectionEnd: ' + this.selectionEnd + ']'; } - public readFromTextArea(textArea: ITextAreaWrapper): TextAreaState { + public static readFromTextArea(textArea: ITextAreaWrapper): TextAreaState { return new TextAreaState(textArea.getValue(), textArea.getSelectionStart(), textArea.getSelectionEnd(), null, null); } @@ -60,7 +60,7 @@ export class TextAreaState { } public writeToTextArea(reason: string, textArea: ITextAreaWrapper, select: boolean): void { - // console.log(Date.now() + ': applyToTextArea ' + reason + ': ' + this.toString()); + // console.log(Date.now() + ': writeToTextArea ' + reason + ': ' + this.toString()); textArea.setValue(reason, this.value); if (select) { textArea.setSelectionRange(reason, this.selectionStart, this.selectionEnd); @@ -97,7 +97,7 @@ export class TextAreaState { return new TextAreaState(text, 0, text.length, null, null); } - public static deduceInput(previousState: TextAreaState, currentState: TextAreaState, couldBeEmojiInput: boolean): ITypeData { + public static deduceInput(previousState: TextAreaState, currentState: TextAreaState, couldBeEmojiInput: boolean, couldBeTypingAtOffset0: boolean): ITypeData { if (!previousState) { // This is the EMPTY state return { @@ -117,6 +117,18 @@ export class TextAreaState { let currentSelectionStart = currentState.selectionStart; let currentSelectionEnd = currentState.selectionEnd; + if (couldBeTypingAtOffset0 && previousValue.length > 0 && previousSelectionStart === previousSelectionEnd && currentSelectionStart === currentSelectionEnd) { + // See https://github.com/Microsoft/vscode/issues/42251 + // where typing always happens at offset 0 in the textarea + // when using a custom title area in OSX and moving the window + if (strings.endsWith(currentValue, previousValue)) { + // Looks like something was typed at offset 0 + // ==> pretend we placed the cursor at offset 0 to begin with... + previousSelectionStart = 0; + previousSelectionEnd = 0; + } + } + // Strip the previous suffix from the value (without interfering with the current selection) const previousSuffix = previousValue.substring(previousSelectionEnd); const currentSuffix = currentValue.substring(currentSelectionEnd); diff --git a/src/vs/editor/browser/editorBrowser.ts b/src/vs/editor/browser/editorBrowser.ts index 79d5d54e20b..18810f9d54f 100644 --- a/src/vs/editor/browser/editorBrowser.ts +++ b/src/vs/editor/browser/editorBrowser.ts @@ -391,6 +391,12 @@ export interface ICodeEditor extends editorCommon.IEditor { * @internal */ onDidType(listener: (text: string) => void): IDisposable; + /** + * An event emitted when editing failed because the editor is read-only. + * @event + * @internal + */ + onDidAttemptReadOnlyEdit(listener: () => void): IDisposable; /** * An event emitted when users paste text in the editor. * @event @@ -619,11 +625,6 @@ export interface ICodeEditor extends editorCommon.IEditor { */ getLayoutInfo(): editorOptions.EditorLayoutInfo; - /** - * Returns the range that is currently centered in the view port. - */ - getCenteredRangeInViewport(): Range; - /** * Returns the ranges that are currently visible. * Does not account for horizontal scrolling. diff --git a/src/vs/editor/browser/services/codeEditorServiceImpl.ts b/src/vs/editor/browser/services/codeEditorServiceImpl.ts index 6f2bde8d93c..64938f0b602 100644 --- a/src/vs/editor/browser/services/codeEditorServiceImpl.ts +++ b/src/vs/editor/browser/services/codeEditorServiceImpl.ts @@ -126,6 +126,7 @@ class DecorationTypeOptionsProvider implements IModelDecorationOptionsProvider { public className: string; public inlineClassName: string; + public inlineClassNameAffectsLetterSpacing: boolean; public beforeContentClassName: string; public afterContentClassName: string; public glyphMarginClassName: string; @@ -145,9 +146,21 @@ class DecorationTypeOptionsProvider implements IModelDecorationOptionsProvider { } return void 0; }; + let createInlineCSSRules = (type: ModelDecorationCSSRuleType) => { + let rules = new DecorationCSSRules(type, providerArgs, themeService); + if (rules.hasContent) { + this._disposables.push(rules); + return { className: rules.className, hasLetterSpacing: rules.hasLetterSpacing }; + } + return null; + }; this.className = createCSSRules(ModelDecorationCSSRuleType.ClassName); - this.inlineClassName = createCSSRules(ModelDecorationCSSRuleType.InlineClassName); + const inlineData = createInlineCSSRules(ModelDecorationCSSRuleType.InlineClassName); + if (inlineData) { + this.inlineClassName = inlineData.className; + this.inlineClassNameAffectsLetterSpacing = inlineData.hasLetterSpacing; + } this.beforeContentClassName = createCSSRules(ModelDecorationCSSRuleType.BeforeContentClassName); this.afterContentClassName = createCSSRules(ModelDecorationCSSRuleType.AfterContentClassName); this.glyphMarginClassName = createCSSRules(ModelDecorationCSSRuleType.GlyphMarginClassName); @@ -194,6 +207,7 @@ class DecorationTypeOptionsProvider implements IModelDecorationOptionsProvider { const _CSS_MAP = { color: 'color:{0} !important;', + opacity: 'opacity:{0};', backgroundColor: 'background-color:{0};', outline: 'outline:{0};', @@ -231,6 +245,7 @@ class DecorationCSSRules { private _className: string; private _unThemedSelector: string; private _hasContent: boolean; + private _hasLetterSpacing: boolean; private _ruleType: ModelDecorationCSSRuleType; private _themeListener: IDisposable; private _providerArgs: ProviderArguments; @@ -242,6 +257,7 @@ class DecorationCSSRules { this._providerArgs = providerArgs; this._usesThemeColors = false; this._hasContent = false; + this._hasLetterSpacing = false; let className = CSSNameHelper.getClassName(this._providerArgs.key, ruleType); if (this._providerArgs.parentTypeKey) { @@ -277,6 +293,10 @@ class DecorationCSSRules { return this._hasContent; } + public get hasLetterSpacing(): boolean { + return this._hasLetterSpacing; + } + public get className(): string { return this._className; } @@ -357,7 +377,10 @@ class DecorationCSSRules { return ''; } let cssTextArr: string[] = []; - this.collectCSSText(opts, ['fontStyle', 'fontWeight', 'textDecoration', 'cursor', 'color', 'letterSpacing'], cssTextArr); + this.collectCSSText(opts, ['fontStyle', 'fontWeight', 'textDecoration', 'cursor', 'color', 'opacity', 'letterSpacing'], cssTextArr); + if (opts.letterSpacing) { + this._hasLetterSpacing = true; + } return cssTextArr.join(''); } @@ -385,7 +408,7 @@ class DecorationCSSRules { cssTextArr.push(strings.format(_CSS_MAP.contentText, escaped)); } - this.collectCSSText(opts, ['fontStyle', 'fontWeight', 'textDecoration', 'color', 'backgroundColor', 'margin'], cssTextArr); + this.collectCSSText(opts, ['fontStyle', 'fontWeight', 'textDecoration', 'color', 'opacity', 'backgroundColor', 'margin'], cssTextArr); if (this.collectCSSText(opts, ['width', 'height'], cssTextArr)) { cssTextArr.push('display:inline-block;'); } diff --git a/src/vs/editor/browser/view/viewController.ts b/src/vs/editor/browser/view/viewController.ts index 8dfebb34b87..31233148de5 100644 --- a/src/vs/editor/browser/view/viewController.ts +++ b/src/vs/editor/browser/view/viewController.ts @@ -31,6 +31,9 @@ export interface IMouseDispatchData { ctrlKey: boolean; metaKey: boolean; shiftKey: boolean; + + leftButton: boolean; + middleButton: boolean; } export interface ICommandDelegate { @@ -133,7 +136,13 @@ export class ViewController { } public dispatchMouse(data: IMouseDispatchData): void { - if (data.startedOnLineNumbers) { + if (data.middleButton) { + if (data.inSelectionMode) { + this.columnSelect(data.position, data.mouseColumn); + } else { + this.moveTo(data.position); + } + } else if (data.startedOnLineNumbers) { // If the dragging started on the gutter, then have operations work on the entire line if (this._hasMulticursorModifier(data)) { if (data.inSelectionMode) { diff --git a/src/vs/editor/browser/view/viewImpl.ts b/src/vs/editor/browser/view/viewImpl.ts index 88fa7e2ed2d..22de5aae513 100644 --- a/src/vs/editor/browser/view/viewImpl.ts +++ b/src/vs/editor/browser/view/viewImpl.ts @@ -321,6 +321,7 @@ export class View extends ViewEventHandler { } public onFocusChanged(e: viewEvents.ViewFocusChangedEvent): boolean { this.domNode.setClassName(this.getEditorClassName()); + this._context.model.setHasFocus(e.isFocused); if (e.isFocused) { this.outgoingEvents.emitViewFocusGained(); } else { diff --git a/src/vs/editor/browser/viewParts/contentWidgets/contentWidgets.ts b/src/vs/editor/browser/viewParts/contentWidgets/contentWidgets.ts index 93934f1131b..6dbaa2e4525 100644 --- a/src/vs/editor/browser/viewParts/contentWidgets/contentWidgets.ts +++ b/src/vs/editor/browser/viewParts/contentWidgets/contentWidgets.ts @@ -308,7 +308,8 @@ class Widget { private _layoutBoxInPage(topLeft: Coordinate, width: number, height: number, ctx: RenderingContext): IBoxLayoutResult { let left0 = topLeft.left - ctx.scrollLeft; - if (left0 + width < 0 || left0 > this._contentWidth) { + if (left0 < 0 || left0 > this._contentWidth) { + // Don't render if position is scrolled outside viewport return null; } diff --git a/src/vs/editor/browser/viewParts/decorations/decorations.ts b/src/vs/editor/browser/viewParts/decorations/decorations.ts index da0359739fe..f72476f8f30 100644 --- a/src/vs/editor/browser/viewParts/decorations/decorations.ts +++ b/src/vs/editor/browser/viewParts/decorations/decorations.ts @@ -85,8 +85,14 @@ export class DecorationsOverlay extends DynamicViewOverlay { // Sort decorations for consistent render output decorations = decorations.sort((a, b) => { - let aClassName = a.options.className; - let bClassName = b.options.className; + if (a.options.zIndex < b.options.zIndex) { + return -1; + } + if (a.options.zIndex > b.options.zIndex) { + return 1; + } + const aClassName = a.options.className; + const bClassName = b.options.className; if (aClassName < bClassName) { return -1; @@ -142,8 +148,12 @@ export class DecorationsOverlay extends DynamicViewOverlay { } private _renderNormalDecorations(ctx: RenderingContext, decorations: ViewModelDecoration[], output: string[]): void { - let lineHeight = String(this._lineHeight); - let visibleStartLineNumber = ctx.visibleRange.startLineNumber; + const lineHeight = String(this._lineHeight); + const visibleStartLineNumber = ctx.visibleRange.startLineNumber; + + let prevClassName: string = null; + let prevShowIfCollapsed: boolean = false; + let prevRange: Range = null; for (let i = 0, lenI = decorations.length; i < lenI; i++) { const d = decorations[i]; @@ -160,39 +170,60 @@ export class DecorationsOverlay extends DynamicViewOverlay { range = new Range(range.startLineNumber, range.startColumn, range.endLineNumber - 1, this._context.model.getLineMaxColumn(range.endLineNumber - 1)); } - let linesVisibleRanges = ctx.linesVisibleRangesForRange(range, /*TODO@Alex*/className === 'findMatch'); - if (!linesVisibleRanges) { + if (prevClassName === className && prevShowIfCollapsed === showIfCollapsed && Range.areIntersectingOrTouching(prevRange, range)) { + // merge into previous decoration + prevRange = Range.plusRange(prevRange, range); continue; } - for (let j = 0, lenJ = linesVisibleRanges.length; j < lenJ; j++) { - let lineVisibleRanges = linesVisibleRanges[j]; - const lineIndex = lineVisibleRanges.lineNumber - visibleStartLineNumber; + // flush previous decoration + if (prevClassName !== null) { + this._renderNormalDecoration(ctx, prevRange, prevClassName, prevShowIfCollapsed, lineHeight, visibleStartLineNumber, output); + } - if (showIfCollapsed && lineVisibleRanges.ranges.length === 1) { - const singleVisibleRange = lineVisibleRanges.ranges[0]; - if (singleVisibleRange.width === 0) { - // collapsed range case => make the decoration visible by faking its width - lineVisibleRanges.ranges[0] = new HorizontalRange(singleVisibleRange.left, this._typicalHalfwidthCharacterWidth); - } - } + prevClassName = className; + prevShowIfCollapsed = showIfCollapsed; + prevRange = range; + } - for (let k = 0, lenK = lineVisibleRanges.ranges.length; k < lenK; k++) { - const visibleRange = lineVisibleRanges.ranges[k]; - const decorationOutput = ( - '
' - ); - output[lineIndex] += decorationOutput; + if (prevClassName !== null) { + this._renderNormalDecoration(ctx, prevRange, prevClassName, prevShowIfCollapsed, lineHeight, visibleStartLineNumber, output); + } + } + + private _renderNormalDecoration(ctx: RenderingContext, range: Range, className: string, showIfCollapsed: boolean, lineHeight: string, visibleStartLineNumber: number, output: string[]): void { + let linesVisibleRanges = ctx.linesVisibleRangesForRange(range, /*TODO@Alex*/className === 'findMatch'); + if (!linesVisibleRanges) { + return; + } + + for (let j = 0, lenJ = linesVisibleRanges.length; j < lenJ; j++) { + let lineVisibleRanges = linesVisibleRanges[j]; + const lineIndex = lineVisibleRanges.lineNumber - visibleStartLineNumber; + + if (showIfCollapsed && lineVisibleRanges.ranges.length === 1) { + const singleVisibleRange = lineVisibleRanges.ranges[0]; + if (singleVisibleRange.width === 0) { + // collapsed range case => make the decoration visible by faking its width + lineVisibleRanges.ranges[0] = new HorizontalRange(singleVisibleRange.left, this._typicalHalfwidthCharacterWidth); } } + + for (let k = 0, lenK = lineVisibleRanges.ranges.length; k < lenK; k++) { + const visibleRange = lineVisibleRanges.ranges[k]; + const decorationOutput = ( + '
' + ); + output[lineIndex] += decorationOutput; + } } } diff --git a/src/vs/editor/browser/viewParts/indentGuides/indentGuides.css b/src/vs/editor/browser/viewParts/indentGuides/indentGuides.css index 45992137d18..3055ad67e4e 100644 --- a/src/vs/editor/browser/viewParts/indentGuides/indentGuides.css +++ b/src/vs/editor/browser/viewParts/indentGuides/indentGuides.css @@ -10,3 +10,6 @@ .monaco-editor .lines-content .cigr { position: absolute; } +.monaco-editor .lines-content .cigra { + position: absolute; +} diff --git a/src/vs/editor/browser/viewParts/indentGuides/indentGuides.ts b/src/vs/editor/browser/viewParts/indentGuides/indentGuides.ts index 8e97e032f30..8834b5fed88 100644 --- a/src/vs/editor/browser/viewParts/indentGuides/indentGuides.ts +++ b/src/vs/editor/browser/viewParts/indentGuides/indentGuides.ts @@ -11,13 +11,13 @@ import { ViewContext } from 'vs/editor/common/view/viewContext'; import { RenderingContext } from 'vs/editor/common/view/renderingContext'; import * as viewEvents from 'vs/editor/common/view/viewEvents'; import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; -import { editorIndentGuides } from 'vs/editor/common/view/editorColorRegistry'; -import * as dom from 'vs/base/browser/dom'; +import { editorIndentGuides, editorActiveIndentGuides } from 'vs/editor/common/view/editorColorRegistry'; import { Position } from 'vs/editor/common/core/position'; export class IndentGuidesOverlay extends DynamicViewOverlay { private _context: ViewContext; + private _primaryLineNumber: number; private _lineHeight: number; private _spaceWidth: number; private _renderResult: string[]; @@ -26,6 +26,7 @@ export class IndentGuidesOverlay extends DynamicViewOverlay { constructor(context: ViewContext) { super(); this._context = context; + this._primaryLineNumber = 0; this._lineHeight = this._context.configuration.editor.lineHeight; this._spaceWidth = this._context.configuration.editor.fontInfo.spaceWidth; this._enabled = this._context.configuration.editor.viewInfo.renderIndentGuides; @@ -55,6 +56,17 @@ export class IndentGuidesOverlay extends DynamicViewOverlay { } return true; } + public onCursorStateChanged(e: viewEvents.ViewCursorStateChangedEvent): boolean { + const selection = e.selections[0]; + const newPrimaryLineNumber = selection.isEmpty() ? selection.positionLineNumber : 0; + + if (this._primaryLineNumber !== newPrimaryLineNumber) { + this._primaryLineNumber = newPrimaryLineNumber; + return true; + } + + return false; + } public onDecorationsChanged(e: viewEvents.ViewDecorationsChangedEvent): boolean { // true for inline decorations return true; @@ -94,20 +106,32 @@ export class IndentGuidesOverlay extends DynamicViewOverlay { const tabSize = this._context.model.getTabSize(); const tabWidth = tabSize * this._spaceWidth; const lineHeight = this._lineHeight; - const indentGuideWidth = dom.computeScreenAwareSize(1); + const indentGuideWidth = tabWidth; const indents = this._context.model.getLinesIndentGuides(visibleStartLineNumber, visibleEndLineNumber); + let activeIndentStartLineNumber = 0; + let activeIndentEndLineNumber = 0; + let activeIndentLevel = 0; + if (this._primaryLineNumber) { + const activeIndentInfo = this._context.model.getActiveIndentGuide(this._primaryLineNumber); + activeIndentStartLineNumber = activeIndentInfo.startLineNumber; + activeIndentEndLineNumber = activeIndentInfo.endLineNumber; + activeIndentLevel = activeIndentInfo.indent; + } + let output: string[] = []; for (let lineNumber = visibleStartLineNumber; lineNumber <= visibleEndLineNumber; lineNumber++) { + const containsActiveIndentGuide = (activeIndentStartLineNumber <= lineNumber && lineNumber <= activeIndentEndLineNumber); const lineIndex = lineNumber - visibleStartLineNumber; const indent = indents[lineIndex]; let result = ''; let leftMostVisiblePosition = ctx.visibleRangeForPosition(new Position(lineNumber, 1)); let left = leftMostVisiblePosition ? leftMostVisiblePosition.left : 0; - for (let i = 0; i < indent; i++) { - result += `
`; + for (let i = 1; i <= indent; i++) { + let className = (containsActiveIndentGuide && i === activeIndentLevel ? 'cigra' : 'cigr'); + result += `
`; left += tabWidth; } @@ -129,8 +153,12 @@ export class IndentGuidesOverlay extends DynamicViewOverlay { } registerThemingParticipant((theme, collector) => { - let editorGuideColor = theme.getColor(editorIndentGuides); - if (editorGuideColor) { - collector.addRule(`.monaco-editor .lines-content .cigr { background-color: ${editorGuideColor}; }`); + let editorIndentGuidesColor = theme.getColor(editorIndentGuides); + if (editorIndentGuidesColor) { + collector.addRule(`.monaco-editor .lines-content .cigr { box-shadow: 1px 0 0 0 ${editorIndentGuidesColor} inset; }`); + } + let editorActiveIndentGuidesColor = theme.getColor(editorActiveIndentGuides) || editorIndentGuidesColor; + if (editorActiveIndentGuidesColor) { + collector.addRule(`.monaco-editor .lines-content .cigra { box-shadow: 1px 0 0 0 ${editorActiveIndentGuidesColor} inset; }`); } }); diff --git a/src/vs/editor/browser/viewParts/lines/viewLine.ts b/src/vs/editor/browser/viewParts/lines/viewLine.ts index 7a9761d20d8..dcfc1dc3ad3 100644 --- a/src/vs/editor/browser/viewParts/lines/viewLine.ts +++ b/src/vs/editor/browser/viewParts/lines/viewLine.ts @@ -6,11 +6,10 @@ import * as browser from 'vs/base/browser/browser'; import * as platform from 'vs/base/common/platform'; -import * as strings from 'vs/base/common/strings'; import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode'; import { IConfiguration } from 'vs/editor/common/editorCommon'; import { LineDecoration } from 'vs/editor/common/viewLayout/lineDecorations'; -import { renderViewLine, RenderLineInput, CharacterMapping } from 'vs/editor/common/viewLayout/viewLineRenderer'; +import { renderViewLine, RenderLineInput, CharacterMapping, ForeignElementType } from 'vs/editor/common/viewLayout/viewLineRenderer'; import { IVisibleLine } from 'vs/editor/browser/view/viewLayer'; import { RangeUtil } from 'vs/editor/browser/viewParts/lines/rangeUtil'; import { HorizontalRange } from 'vs/editor/common/view/renderingContext'; @@ -192,7 +191,8 @@ export class ViewLine implements IVisibleLine { let renderLineInput = new RenderLineInput( options.useMonospaceOptimizations, lineData.content, - lineData.mightContainRTL, + lineData.isBasicASCII, + lineData.containsRTL, lineData.minColumn - 1, lineData.tokens, actualInlineDecorations, @@ -222,18 +222,16 @@ export class ViewLine implements IVisibleLine { sb.appendASCIIString(''); let renderedViewLine: IRenderedViewLine = null; - if (canUseFastRenderedViewLine && options.useMonospaceOptimizations && !output.containsForeignElements) { - let isRegularASCII = true; - if (lineData.mightContainNonBasicASCII) { - isRegularASCII = strings.isBasicASCII(lineData.content); - } - - if (isRegularASCII && lineData.content.length < 1000 && renderLineInput.lineTokens.getCount() < 100) { + if (canUseFastRenderedViewLine && lineData.isBasicASCII && options.useMonospaceOptimizations && output.containsForeignElements === ForeignElementType.None) { + if (lineData.content.length < 300 && renderLineInput.lineTokens.getCount() < 100) { // Browser rounding errors have been observed in Chrome and IE, so using the fast // view line only for short lines. Please test before removing the length check... // --- // Another rounding error has been observed on Linux in VSCode, where width // rounding errors add up to an observable large number... + // --- + // Also see another example of rounding errors on Windows in + // https://github.com/Microsoft/vscode/issues/33178 renderedViewLine = new FastRenderedViewLine( this._renderedViewLine ? this._renderedViewLine.domNode : null, renderLineInput, @@ -385,7 +383,7 @@ class RenderedViewLine implements IRenderedViewLine { protected readonly _characterMapping: CharacterMapping; private readonly _isWhitespaceOnly: boolean; - private readonly _containsForeignElements: boolean; + private readonly _containsForeignElements: ForeignElementType; private _cachedWidth: number; /** @@ -393,7 +391,7 @@ class RenderedViewLine implements IRenderedViewLine { */ private _pixelOffsetCache: Int32Array; - constructor(domNode: FastDomNode, renderLineInput: RenderLineInput, characterMapping: CharacterMapping, containsRTL: boolean, containsForeignElements: boolean) { + constructor(domNode: FastDomNode, renderLineInput: RenderLineInput, characterMapping: CharacterMapping, containsRTL: boolean, containsForeignElements: ForeignElementType) { this.domNode = domNode; this.input = renderLineInput; this._characterMapping = characterMapping; @@ -471,10 +469,18 @@ class RenderedViewLine implements IRenderedViewLine { protected _readPixelOffset(column: number, context: DomReadingContext): number { if (this._characterMapping.length === 0) { // This line has no content - if (!this._containsForeignElements) { + if (this._containsForeignElements === ForeignElementType.None) { // We can assume the line is really empty return 0; } + if (this._containsForeignElements === ForeignElementType.After) { + // We have foreign elements after the (empty) line + return 0; + } + if (this._containsForeignElements === ForeignElementType.Before) { + // We have foreign element before the (empty) line + return this.getWidth(); + } } if (this._pixelOffsetCache !== null) { @@ -503,7 +509,7 @@ class RenderedViewLine implements IRenderedViewLine { return r[0].left; } - if (column === this._characterMapping.length && this._isWhitespaceOnly && !this._containsForeignElements) { + if (column === this._characterMapping.length && this._isWhitespaceOnly && this._containsForeignElements === ForeignElementType.None) { // This branch helps in the case of whitespace only lines which have a width set return this.getWidth(); } @@ -586,17 +592,17 @@ class WebKitRenderedViewLine extends RenderedViewLine { } } -const createRenderedLine: (domNode: FastDomNode, renderLineInput: RenderLineInput, characterMapping: CharacterMapping, containsRTL: boolean, containsForeignElements: boolean) => RenderedViewLine = (function () { +const createRenderedLine: (domNode: FastDomNode, renderLineInput: RenderLineInput, characterMapping: CharacterMapping, containsRTL: boolean, containsForeignElements: ForeignElementType) => RenderedViewLine = (function () { if (browser.isWebKit) { return createWebKitRenderedLine; } return createNormalRenderedLine; })(); -function createWebKitRenderedLine(domNode: FastDomNode, renderLineInput: RenderLineInput, characterMapping: CharacterMapping, containsRTL: boolean, containsForeignElements: boolean): RenderedViewLine { +function createWebKitRenderedLine(domNode: FastDomNode, renderLineInput: RenderLineInput, characterMapping: CharacterMapping, containsRTL: boolean, containsForeignElements: ForeignElementType): RenderedViewLine { return new WebKitRenderedViewLine(domNode, renderLineInput, characterMapping, containsRTL, containsForeignElements); } -function createNormalRenderedLine(domNode: FastDomNode, renderLineInput: RenderLineInput, characterMapping: CharacterMapping, containsRTL: boolean, containsForeignElements: boolean): RenderedViewLine { +function createNormalRenderedLine(domNode: FastDomNode, renderLineInput: RenderLineInput, characterMapping: CharacterMapping, containsRTL: boolean, containsForeignElements: ForeignElementType): RenderedViewLine { return new RenderedViewLine(domNode, renderLineInput, characterMapping, containsRTL, containsForeignElements); } diff --git a/src/vs/editor/browser/viewParts/margin/margin.ts b/src/vs/editor/browser/viewParts/margin/margin.ts index 40efb4b4ef7..63a228dea04 100644 --- a/src/vs/editor/browser/viewParts/margin/margin.ts +++ b/src/vs/editor/browser/viewParts/margin/margin.ts @@ -14,6 +14,7 @@ import * as viewEvents from 'vs/editor/common/view/viewEvents'; export class Margin extends ViewPart { public static readonly CLASS_NAME = 'glyph-margin'; + public static readonly OUTER_CLASS_NAME = 'margin'; private _domNode: FastDomNode; private _canUseLayerHinting: boolean; @@ -42,7 +43,7 @@ export class Margin extends ViewPart { private _createDomNode(): FastDomNode { let domNode = createFastDomNode(document.createElement('div')); - domNode.setClassName('margin'); + domNode.setClassName(Margin.OUTER_CLASS_NAME); domNode.setPosition('absolute'); domNode.setAttribute('role', 'presentation'); domNode.setAttribute('aria-hidden', 'true'); diff --git a/src/vs/editor/browser/viewParts/minimap/minimap.ts b/src/vs/editor/browser/viewParts/minimap/minimap.ts index deebf469211..35ad7ff531b 100644 --- a/src/vs/editor/browser/viewParts/minimap/minimap.ts +++ b/src/vs/editor/browser/viewParts/minimap/minimap.ts @@ -7,6 +7,7 @@ import 'vs/css!./minimap'; import { ViewPart, PartFingerprint, PartFingerprints } from 'vs/editor/browser/view/viewPart'; +import * as strings from 'vs/base/common/strings'; import { ViewContext } from 'vs/editor/common/view/viewContext'; import { RenderingContext, RestrictedRenderingContext } from 'vs/editor/common/view/renderingContext'; import { getOrCreateMinimapCharRenderer } from 'vs/editor/common/view/runtimeMinimapCharRenderer'; @@ -897,17 +898,22 @@ export class Minimap extends ViewPart { // No need to render anything since space is invisible dx += charWidth; } else { - if (renderMinimap === RenderMinimap.Large) { - minimapCharRenderer.x2RenderChar(target, dx, dy, charCode, tokenColor, backgroundColor, useLighterFont); - } else if (renderMinimap === RenderMinimap.Small) { - minimapCharRenderer.x1RenderChar(target, dx, dy, charCode, tokenColor, backgroundColor, useLighterFont); - } else if (renderMinimap === RenderMinimap.LargeBlocks) { - minimapCharRenderer.x2BlockRenderChar(target, dx, dy, tokenColor, backgroundColor, useLighterFont); - } else { - // RenderMinimap.SmallBlocks - minimapCharRenderer.x1BlockRenderChar(target, dx, dy, tokenColor, backgroundColor, useLighterFont); + // Render twice for a full width character + let count = strings.isFullWidthCharacter(charCode) ? 2 : 1; + + for (let i = 0; i < count; i++) { + if (renderMinimap === RenderMinimap.Large) { + minimapCharRenderer.x2RenderChar(target, dx, dy, charCode, tokenColor, backgroundColor, useLighterFont); + } else if (renderMinimap === RenderMinimap.Small) { + minimapCharRenderer.x1RenderChar(target, dx, dy, charCode, tokenColor, backgroundColor, useLighterFont); + } else if (renderMinimap === RenderMinimap.LargeBlocks) { + minimapCharRenderer.x2BlockRenderChar(target, dx, dy, tokenColor, backgroundColor, useLighterFont); + } else { + // RenderMinimap.SmallBlocks + minimapCharRenderer.x1BlockRenderChar(target, dx, dy, tokenColor, backgroundColor, useLighterFont); + } + dx += charWidth; } - dx += charWidth; } } } diff --git a/src/vs/editor/browser/viewParts/rulers/rulers.ts b/src/vs/editor/browser/viewParts/rulers/rulers.ts index 036ff988cb7..bac590e8174 100644 --- a/src/vs/editor/browser/viewParts/rulers/rulers.ts +++ b/src/vs/editor/browser/viewParts/rulers/rulers.ts @@ -13,7 +13,6 @@ import { RenderingContext, RestrictedRenderingContext } from 'vs/editor/common/v import * as viewEvents from 'vs/editor/common/view/viewEvents'; import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { editorRuler } from 'vs/editor/common/view/editorColorRegistry'; -import * as dom from 'vs/base/browser/dom'; export class Rulers extends ViewPart { @@ -67,7 +66,7 @@ export class Rulers extends ViewPart { } if (currentCount < desiredCount) { - const rulerWidth = dom.computeScreenAwareSize(1); + const rulerWidth = this._context.model.getTabSize(); let addCount = desiredCount - currentCount; while (addCount > 0) { let node = createFastDomNode(document.createElement('div')); @@ -104,6 +103,6 @@ export class Rulers extends ViewPart { registerThemingParticipant((theme, collector) => { let rulerColor = theme.getColor(editorRuler); if (rulerColor) { - collector.addRule(`.monaco-editor .view-ruler { background-color: ${rulerColor}; }`); + collector.addRule(`.monaco-editor .view-ruler { box-shadow: 1px 0 0 0 ${rulerColor} inset; }`); } }); diff --git a/src/vs/editor/browser/viewParts/selections/selections.ts b/src/vs/editor/browser/viewParts/selections/selections.ts index c844dfc3516..a3e1d9e1dec 100644 --- a/src/vs/editor/browser/viewParts/selections/selections.ts +++ b/src/vs/editor/browser/viewParts/selections/selections.ts @@ -78,6 +78,7 @@ export class SelectionsOverlay extends DynamicViewOverlay { private _context: ViewContext; private _lineHeight: number; private _roundedSelection: boolean; + private _typicalHalfwidthCharacterWidth: number; private _selections: Range[]; private _renderResult: string[]; @@ -86,6 +87,7 @@ export class SelectionsOverlay extends DynamicViewOverlay { this._context = context; this._lineHeight = this._context.configuration.editor.lineHeight; this._roundedSelection = this._context.configuration.editor.viewInfo.roundedSelection; + this._typicalHalfwidthCharacterWidth = this._context.configuration.editor.fontInfo.typicalHalfwidthCharacterWidth; this._selections = []; this._renderResult = null; this._context.addEventHandler(this); @@ -108,6 +110,9 @@ export class SelectionsOverlay extends DynamicViewOverlay { if (e.viewInfo) { this._roundedSelection = this._context.configuration.editor.viewInfo.roundedSelection; } + if (e.fontInfo) { + this._typicalHalfwidthCharacterWidth = this._context.configuration.editor.fontInfo.typicalHalfwidthCharacterWidth; + } return true; } public onCursorStateChanged(e: viewEvents.ViewCursorStateChangedEvent): boolean { @@ -153,23 +158,28 @@ export class SelectionsOverlay extends DynamicViewOverlay { return false; } - private _enrichVisibleRangesWithStyle(linesVisibleRanges: LineVisibleRangesWithStyle[], previousFrame: LineVisibleRangesWithStyle[]): void { + private _enrichVisibleRangesWithStyle(viewport: Range, linesVisibleRanges: LineVisibleRangesWithStyle[], previousFrame: LineVisibleRangesWithStyle[]): void { + const epsilon = this._typicalHalfwidthCharacterWidth / 4; let previousFrameTop: HorizontalRangeWithStyle = null; let previousFrameBottom: HorizontalRangeWithStyle = null; if (previousFrame && previousFrame.length > 0 && linesVisibleRanges.length > 0) { let topLineNumber = linesVisibleRanges[0].lineNumber; - for (let i = 0; !previousFrameTop && i < previousFrame.length; i++) { - if (previousFrame[i].lineNumber === topLineNumber) { - previousFrameTop = previousFrame[i].ranges[0]; + if (topLineNumber === viewport.startLineNumber) { + for (let i = 0; !previousFrameTop && i < previousFrame.length; i++) { + if (previousFrame[i].lineNumber === topLineNumber) { + previousFrameTop = previousFrame[i].ranges[0]; + } } } let bottomLineNumber = linesVisibleRanges[linesVisibleRanges.length - 1].lineNumber; - for (let i = previousFrame.length - 1; !previousFrameBottom && i >= 0; i--) { - if (previousFrame[i].lineNumber === bottomLineNumber) { - previousFrameBottom = previousFrame[i].ranges[0]; + if (bottomLineNumber === viewport.endLineNumber) { + for (let i = previousFrame.length - 1; !previousFrameBottom && i >= 0; i--) { + if (previousFrame[i].lineNumber === bottomLineNumber) { + previousFrameBottom = previousFrame[i].ranges[0]; + } } } @@ -202,13 +212,13 @@ export class SelectionsOverlay extends DynamicViewOverlay { let prevLeft = linesVisibleRanges[i - 1].ranges[0].left; let prevRight = linesVisibleRanges[i - 1].ranges[0].left + linesVisibleRanges[i - 1].ranges[0].width; - if (curLeft === prevLeft) { + if (abs(curLeft - prevLeft) < epsilon) { startStyle.top = CornerStyle.FLAT; } else if (curLeft > prevLeft) { startStyle.top = CornerStyle.INTERN; } - if (curRight === prevRight) { + if (abs(curRight - prevRight) < epsilon) { endStyle.top = CornerStyle.FLAT; } else if (prevLeft < curRight && curRight < prevRight) { endStyle.top = CornerStyle.INTERN; @@ -224,13 +234,13 @@ export class SelectionsOverlay extends DynamicViewOverlay { let nextLeft = linesVisibleRanges[i + 1].ranges[0].left; let nextRight = linesVisibleRanges[i + 1].ranges[0].left + linesVisibleRanges[i + 1].ranges[0].width; - if (curLeft === nextLeft) { + if (abs(curLeft - nextLeft) < epsilon) { startStyle.bottom = CornerStyle.FLAT; } else if (nextLeft < curLeft && curLeft < nextRight) { startStyle.bottom = CornerStyle.INTERN; } - if (curRight === nextRight) { + if (abs(curRight - nextRight) < epsilon) { endStyle.bottom = CornerStyle.FLAT; } else if (curRight < nextRight) { endStyle.bottom = CornerStyle.INTERN; @@ -252,7 +262,7 @@ export class SelectionsOverlay extends DynamicViewOverlay { let visibleRangesHaveGaps = this._visibleRangesHaveGaps(linesVisibleRanges); if (!isIEWithZoomingIssuesNearRoundedBorders && !visibleRangesHaveGaps && this._roundedSelection) { - this._enrichVisibleRangesWithStyle(linesVisibleRanges, previousFrame); + this._enrichVisibleRangesWithStyle(ctx.visibleRange, linesVisibleRanges, previousFrame); } // The visible ranges are sorted TOP-BOTTOM and LEFT-RIGHT @@ -407,3 +417,7 @@ registerThemingParticipant((theme, collector) => { collector.addRule(`.monaco-editor .view-line span.inline-selected-text { color: ${editorSelectionForegroundColor}; }`); } }); + +function abs(n: number): number { + return n < 0 ? -n : n; +} \ No newline at end of file diff --git a/src/vs/editor/browser/viewParts/viewZones/viewZones.ts b/src/vs/editor/browser/viewParts/viewZones/viewZones.ts index 008e7c65129..175b237a2dd 100644 --- a/src/vs/editor/browser/viewParts/viewZones/viewZones.ts +++ b/src/vs/editor/browser/viewParts/viewZones/viewZones.ts @@ -99,7 +99,11 @@ export class ViewZones extends ViewPart { } public onLineMappingChanged(e: viewEvents.ViewLineMappingChangedEvent): boolean { - return this._recomputeWhitespacesProps(); + const hadAChange = this._recomputeWhitespacesProps(); + if (hadAChange) { + this._context.viewLayout.onHeightMaybeChanged(); + } + return hadAChange; } public onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): boolean { diff --git a/src/vs/editor/browser/widget/codeEditorWidget.ts b/src/vs/editor/browser/widget/codeEditorWidget.ts index 8c517a711d6..7103fb97b39 100644 --- a/src/vs/editor/browser/widget/codeEditorWidget.ts +++ b/src/vs/editor/browser/widget/codeEditorWidget.ts @@ -34,6 +34,7 @@ import { IMouseEvent } from 'vs/base/browser/mouseEvent'; import { ClassName } from 'vs/editor/common/model/intervalTree'; import { ITextModel, IModelDecorationOptions } from 'vs/editor/common/model'; import { ICommandDelegate } from 'vs/editor/browser/view/viewController'; +import { INotificationService } from 'vs/platform/notification/common/notification'; export abstract class CodeEditorWidget extends CommonCodeEditor implements editorBrowser.ICodeEditor { @@ -92,22 +93,17 @@ export abstract class CodeEditorWidget extends CommonCodeEditor implements edito @ICodeEditorService codeEditorService: ICodeEditorService, @ICommandService commandService: ICommandService, @IContextKeyService contextKeyService: IContextKeyService, - @IThemeService themeService: IThemeService + @IThemeService themeService: IThemeService, + @INotificationService notificationService: INotificationService ) { - super(domElement, options, isSimpleWidget, instantiationService, contextKeyService); + super(domElement, options, isSimpleWidget, instantiationService, contextKeyService, notificationService); this._codeEditorService = codeEditorService; this._commandService = commandService; this._themeService = themeService; this._focusTracker = new CodeEditorWidgetFocusTracker(domElement); this._focusTracker.onChange(() => { - let hasFocus = this._focusTracker.hasFocus(); - - if (hasFocus) { - this._onDidFocusEditor.fire(); - } else { - this._onDidBlurEditor.fire(); - } + this._editorFocus.setValue(this._focusTracker.hasFocus()); }); this.contentWidgets = {}; @@ -431,13 +427,13 @@ export abstract class CodeEditorWidget extends CommonCodeEditor implements edito const viewEventBus = this._view.getInternalEventBus(); viewEventBus.onDidGainFocus = () => { - this._onDidFocusEditorText.fire(); + this._editorTextFocus.setValue(true); // In IE, the focus is not synchronous, so we give it a little help - this._onDidFocusEditor.fire(); + this._editorFocus.setValue(true); }; viewEventBus.onDidScroll = (e) => this._onDidScrollChange.fire(e); - viewEventBus.onDidLoseFocus = () => this._onDidBlurEditorText.fire(); + viewEventBus.onDidLoseFocus = () => this._editorTextFocus.setValue(false); viewEventBus.onContextMenu = (e) => this._onContextMenu.fire(e); viewEventBus.onMouseDown = (e) => this._onMouseDown.fire(e); viewEventBus.onMouseUp = (e) => this._onMouseUp.fire(e); @@ -455,11 +451,11 @@ export abstract class CodeEditorWidget extends CommonCodeEditor implements edito return; } if (s && s.cursorState && s.viewState) { - const reducedState = this.viewModel.viewLayout.reduceRestoreState(s.viewState); + const reducedState = this.viewModel.reduceRestoreState(s.viewState); const linesViewportData = this.viewModel.viewLayout.getLinesViewportDataAtScrollTop(reducedState.scrollTop); - const startViewPosition = this.viewModel.coordinatesConverter.convertViewPositionToModelPosition(new Position(linesViewportData.startLineNumber, 1)); - const endViewPosition = this.viewModel.coordinatesConverter.convertViewPositionToModelPosition(new Position(linesViewportData.endLineNumber, 1)); - this.model.tokenizeViewport(startViewPosition.lineNumber, endViewPosition.lineNumber); + const startPosition = this.viewModel.coordinatesConverter.convertViewPositionToModelPosition(new Position(linesViewportData.startLineNumber, 1)); + const endPosition = this.viewModel.coordinatesConverter.convertViewPositionToModelPosition(new Position(linesViewportData.endLineNumber, 1)); + this.model.tokenizeViewport(startPosition.lineNumber, endPosition.lineNumber); this._view.restoreState(reducedState); } } diff --git a/src/vs/editor/browser/widget/diffEditorWidget.ts b/src/vs/editor/browser/widget/diffEditorWidget.ts index 8bd844fdf8e..fd92e00b778 100644 --- a/src/vs/editor/browser/widget/diffEditorWidget.ts +++ b/src/vs/editor/browser/widget/diffEditorWidget.ts @@ -27,7 +27,7 @@ import { LineTokens } from 'vs/editor/common/core/lineTokens'; import { Configuration } from 'vs/editor/browser/config/configuration'; import { Position, IPosition } from 'vs/editor/common/core/position'; import { Selection, ISelection } from 'vs/editor/common/core/selection'; -import { InlineDecoration, InlineDecorationType } from 'vs/editor/common/viewModel/viewModel'; +import { InlineDecoration, InlineDecorationType, ViewLineRenderingData } from 'vs/editor/common/viewModel/viewModel'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { ColorId, MetadataConsts, FontStyle } from 'vs/editor/common/modes'; import { Event, Emitter } from 'vs/base/common/event'; @@ -99,12 +99,7 @@ class VisualEditorState { this._zonesMap = {}; // (2) Model decorations - if (this._decorations.length > 0) { - editor.changeDecorations((changeAccessor: IModelDecorationsChangeAccessor) => { - changeAccessor.deltaDecorations(this._decorations, []); - }); - } - this._decorations = []; + this._decorations = editor.deltaDecorations(this._decorations, []); } public apply(editor: CodeEditor, overviewRuler: editorBrowser.IOverviewRuler, newDecorations: IEditorDiffDecorationsWithZones): void { @@ -1998,10 +1993,13 @@ class InlineViewZonesComputer extends ViewZonesComputer { sb.appendASCIIString(String(count * config.lineHeight)); sb.appendASCIIString('px;width:1000000px;">'); + const isBasicASCII = ViewLineRenderingData.isBasicASCII(lineContent, originalModel.mightContainNonBasicASCII()); + const containsRTL = ViewLineRenderingData.containsRTL(lineContent, isBasicASCII, originalModel.mightContainRTL()); renderViewLine(new RenderLineInput( (config.fontInfo.isMonospace && !config.viewInfo.disableMonospaceOptimizations), lineContent, - originalModel.mightContainRTL(), + isBasicASCII, + containsRTL, 0, lineTokens, actualDecorations, @@ -2032,27 +2030,31 @@ function createFakeLinesDiv(): HTMLElement { } registerThemingParticipant((theme, collector) => { - let added = theme.getColor(diffInserted); + const added = theme.getColor(diffInserted); if (added) { collector.addRule(`.monaco-editor .line-insert, .monaco-editor .char-insert { background-color: ${added}; }`); collector.addRule(`.monaco-diff-editor .line-insert, .monaco-diff-editor .char-insert { background-color: ${added}; }`); collector.addRule(`.monaco-editor .inline-added-margin-view-zone { background-color: ${added}; }`); } - let removed = theme.getColor(diffRemoved); + + const removed = theme.getColor(diffRemoved); if (removed) { collector.addRule(`.monaco-editor .line-delete, .monaco-editor .char-delete { background-color: ${removed}; }`); collector.addRule(`.monaco-diff-editor .line-delete, .monaco-diff-editor .char-delete { background-color: ${removed}; }`); collector.addRule(`.monaco-editor .inline-deleted-margin-view-zone { background-color: ${removed}; }`); } - let addedOutline = theme.getColor(diffInsertedOutline); + + const addedOutline = theme.getColor(diffInsertedOutline); if (addedOutline) { - collector.addRule(`.monaco-editor .line-insert, .monaco-editor .char-insert { border: 1px dashed ${addedOutline}; }`); + collector.addRule(`.monaco-editor .line-insert, .monaco-editor .char-insert { border: 1px ${theme.type === 'hc' ? 'dashed' : 'solid'} ${addedOutline}; }`); } - let removedOutline = theme.getColor(diffRemovedOutline); + + const removedOutline = theme.getColor(diffRemovedOutline); if (removedOutline) { - collector.addRule(`.monaco-editor .line-delete, .monaco-editor .char-delete { border: 1px dashed ${removedOutline}; }`); + collector.addRule(`.monaco-editor .line-delete, .monaco-editor .char-delete { border: 1px ${theme.type === 'hc' ? 'dashed' : 'solid'} ${removedOutline}; }`); } - let shadow = theme.getColor(scrollbarShadow); + + const shadow = theme.getColor(scrollbarShadow); if (shadow) { collector.addRule(`.monaco-diff-editor.side-by-side .editor.modified { box-shadow: -6px 0 5px -5px ${shadow}; }`); } diff --git a/src/vs/editor/browser/widget/diffReview.ts b/src/vs/editor/browser/widget/diffReview.ts index 0f6af96eedd..f1d0c81f619 100644 --- a/src/vs/editor/browser/widget/diffReview.ts +++ b/src/vs/editor/browser/widget/diffReview.ts @@ -29,6 +29,7 @@ import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { ITextModel, TextModelResolvedOptions } from 'vs/editor/common/model'; +import { ViewLineRenderingData } from 'vs/editor/common/viewModel/viewModel'; const DIFF_LINES_PADDING = 3; @@ -583,9 +584,34 @@ export class DiffReview extends Disposable { let cell = document.createElement('div'); cell.className = 'diff-review-cell diff-review-summary'; - cell.appendChild(document.createTextNode(`${diffIndex + 1}/${this._diffs.length}: @@ -${minOriginalLine},${maxOriginalLine - minOriginalLine + 1} +${minModifiedLine},${maxModifiedLine - minModifiedLine + 1} @@`)); + const originalChangedLinesCnt = maxOriginalLine - minOriginalLine + 1; + const modifiedChangedLinesCnt = maxModifiedLine - minModifiedLine + 1; + cell.appendChild(document.createTextNode(`${diffIndex + 1}/${this._diffs.length}: @@ -${minOriginalLine},${originalChangedLinesCnt} +${minModifiedLine},${modifiedChangedLinesCnt} @@`)); header.setAttribute('data-line', String(minModifiedLine)); - header.setAttribute('aria-label', nls.localize('header', "Difference {0} of {1}: original {2}, {3} lines, modified {4}, {5} lines", (diffIndex + 1), this._diffs.length, minOriginalLine, maxOriginalLine - minOriginalLine + 1, minModifiedLine, maxModifiedLine - minModifiedLine + 1)); + + const getAriaLines = (lines: number) => { + if (lines === 0) { + return nls.localize('no_lines', "no lines"); + } else if (lines === 1) { + return nls.localize('one_line', "1 line"); + } else { + return nls.localize('more_lines', "{0} lines", lines); + } + }; + + const originalChangedLinesCntAria = getAriaLines(originalChangedLinesCnt); + const modifiedChangedLinesCntAria = getAriaLines(modifiedChangedLinesCnt); + header.setAttribute('aria-label', nls.localize({ + key: 'header', + comment: [ + 'This is the ARIA label for a git diff header.', + 'A git diff header looks like this: @@ -154,12 +159,39 @@.', + 'That encodes that at original line 154 (which is now line 159), 12 lines were removed/changed with 39 lines.', + 'Variables 0 and 1 refer to the diff index out of total number of diffs.', + 'Variables 2 and 4 will be numbers (a line number).', + 'Variables 3 and 4 will be "no lines", "1 line" or "X lines", localized separately.' + ] + }, "Difference {0} of {1}: original {2}, {3}, modified {4}, {5}", (diffIndex + 1), this._diffs.length, minOriginalLine, originalChangedLinesCntAria, minModifiedLine, modifiedChangedLinesCntAria)); header.appendChild(cell); // @@ -504,7 +517,7 @@ @@ -738,10 +764,13 @@ export class DiffReview extends Disposable { const lineTokens = new LineTokens(tokens, lineContent); + const isBasicASCII = ViewLineRenderingData.isBasicASCII(lineContent, model.mightContainNonBasicASCII()); + const containsRTL = ViewLineRenderingData.containsRTL(lineContent, isBasicASCII, model.mightContainRTL()); const r = renderViewLine(new RenderLineInput( (config.fontInfo.isMonospace && !config.viewInfo.disableMonospaceOptimizations), lineContent, - model.mightContainRTL(), + isBasicASCII, + containsRTL, 0, lineTokens, [], diff --git a/src/vs/editor/browser/widget/embeddedCodeEditorWidget.ts b/src/vs/editor/browser/widget/embeddedCodeEditorWidget.ts index 68a61cec024..b24d4a9b62d 100644 --- a/src/vs/editor/browser/widget/embeddedCodeEditorWidget.ts +++ b/src/vs/editor/browser/widget/embeddedCodeEditorWidget.ts @@ -30,9 +30,10 @@ export class EmbeddedCodeEditorWidget extends CodeEditor { @ICodeEditorService codeEditorService: ICodeEditorService, @ICommandService commandService: ICommandService, @IContextKeyService contextKeyService: IContextKeyService, - @IThemeService themeService: IThemeService + @IThemeService themeService: IThemeService, + @INotificationService notificationService: INotificationService ) { - super(domElement, parentEditor.getRawConfiguration(), instantiationService, codeEditorService, commandService, contextKeyService, themeService); + super(domElement, parentEditor.getRawConfiguration(), instantiationService, codeEditorService, commandService, contextKeyService, themeService, notificationService); this._parentEditor = parentEditor; this._overwriteOptions = options; diff --git a/src/vs/editor/common/commonCodeEditor.ts b/src/vs/editor/common/commonCodeEditor.ts index 91af610e886..99b0be6b3aa 100644 --- a/src/vs/editor/common/commonCodeEditor.ts +++ b/src/vs/editor/common/commonCodeEditor.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; +import * as nls from 'vs/nls'; import { onUnexpectedError } from 'vs/base/common/errors'; import { Event, Emitter } from 'vs/base/common/event'; import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle'; @@ -30,6 +31,7 @@ import { IEditorWhitespace } from 'vs/editor/common/viewLayout/whitespaceCompute import * as modes from 'vs/editor/common/modes'; import { Schemas } from 'vs/base/common/network'; import { ITextModel, EndOfLinePreference, IIdentifiedSingleEditOperation, IModelDecorationsChangeAccessor, IModelDecoration, IModelDeltaDecoration, IModelDecorationOptions } from 'vs/editor/common/model'; +import { INotificationService } from 'vs/platform/notification/common/notification'; let EDITOR_ID = 0; @@ -65,20 +67,19 @@ export abstract class CommonCodeEditor extends Disposable { private readonly _onDidChangeCursorSelection: Emitter = this._register(new Emitter()); public readonly onDidChangeCursorSelection: Event = this._onDidChangeCursorSelection.event; + private readonly _onDidAttemptReadOnlyEdit: Emitter = this._register(new Emitter()); + public readonly onDidAttemptReadOnlyEdit: Event = this._onDidAttemptReadOnlyEdit.event; + private readonly _onDidLayoutChange: Emitter = this._register(new Emitter()); public readonly onDidLayoutChange: Event = this._onDidLayoutChange.event; - protected readonly _onDidFocusEditorText: Emitter = this._register(new Emitter()); - public readonly onDidFocusEditorText: Event = this._onDidFocusEditorText.event; + protected _editorTextFocus: BooleanEventEmitter = this._register(new BooleanEventEmitter()); + public readonly onDidFocusEditorText: Event = this._editorTextFocus.onDidChangeToTrue; + public readonly onDidBlurEditorText: Event = this._editorTextFocus.onDidChangeToFalse; - protected readonly _onDidBlurEditorText: Emitter = this._register(new Emitter()); - public readonly onDidBlurEditorText: Event = this._onDidBlurEditorText.event; - - protected readonly _onDidFocusEditor: Emitter = this._register(new Emitter()); - public readonly onDidFocusEditor: Event = this._onDidFocusEditor.event; - - protected readonly _onDidBlurEditor: Emitter = this._register(new Emitter()); - public readonly onDidBlurEditor: Event = this._onDidBlurEditor.event; + protected _editorFocus: BooleanEventEmitter = this._register(new BooleanEventEmitter()); + public readonly onDidFocusEditor: Event = this._editorFocus.onDidChangeToTrue; + public readonly onDidBlurEditor: Event = this._editorFocus.onDidChangeToFalse; private readonly _onWillType: Emitter = this._register(new Emitter()); public readonly onWillType = this._onWillType.event; @@ -108,6 +109,7 @@ export abstract class CommonCodeEditor extends Disposable { protected readonly _instantiationService: IInstantiationService; protected readonly _contextKeyService: IContextKeyService; + protected readonly _notificationService: INotificationService; /** * map from "parent" decoration type to live decoration ids. @@ -121,7 +123,8 @@ export abstract class CommonCodeEditor extends Disposable { options: editorOptions.IEditorOptions, isSimpleWidget: boolean, instantiationService: IInstantiationService, - contextKeyService: IContextKeyService + contextKeyService: IContextKeyService, + notificationService: INotificationService, ) { super(); this.domElement = domElement; @@ -141,6 +144,7 @@ export abstract class CommonCodeEditor extends Disposable { })); this._contextKeyService = this._register(contextKeyService.createScoped(this.domElement)); + this._notificationService = notificationService; this._register(new EditorContextKeysManager(this, this._contextKeyService)); this._register(new EditorModeContext(this, this._contextKeyService)); @@ -252,13 +256,6 @@ export abstract class CommonCodeEditor extends Disposable { } } - public getCenteredRangeInViewport(): Range { - if (!this.hasView) { - return null; - } - return this.viewModel.getCenteredRangeInViewport(); - } - public getVisibleRanges(): Range[] { if (!this.hasView) { return []; @@ -641,7 +638,7 @@ export abstract class CommonCodeEditor extends Disposable { } const cursorState = this.cursor.saveState(); - const viewState = this.viewModel.viewLayout.saveState(); + const viewState = this.viewModel.saveState(); return { cursorState: cursorState, viewState: viewState, @@ -801,7 +798,7 @@ export abstract class CommonCodeEditor extends Disposable { } this.model.pushEditOperations(this.cursor.getSelections(), edits, () => { - return endCursorState ? endCursorState : this.cursor.getSelections(); + return endCursorState ? endCursorState : null; }); if (endCursorState) { @@ -970,6 +967,14 @@ export abstract class CommonCodeEditor extends Disposable { this._createView(); + this.listenersToRemove.push(this.cursor.onDidReachMaxCursorCount(() => { + this._notificationService.warn(nls.localize('cursors.maximum', "The number of cursors has been limited to {0}.", Cursor.MAX_CURSOR_COUNT)); + })); + + this.listenersToRemove.push(this.cursor.onDidAttemptReadOnlyEdit(() => { + this._onDidAttemptReadOnlyEdit.fire(void 0); + })); + this.listenersToRemove.push(this.cursor.onDidChange((e: CursorStateChangedEvent) => { let positions: Position[] = []; @@ -1047,6 +1052,40 @@ export abstract class CommonCodeEditor extends Disposable { } } +const enum BooleanEventValue { + NotSet, + False, + True +} + +export class BooleanEventEmitter extends Disposable { + private readonly _onDidChangeToTrue: Emitter = this._register(new Emitter()); + public readonly onDidChangeToTrue: Event = this._onDidChangeToTrue.event; + + private readonly _onDidChangeToFalse: Emitter = this._register(new Emitter()); + public readonly onDidChangeToFalse: Event = this._onDidChangeToFalse.event; + + private _value: BooleanEventValue; + + constructor() { + super(); + this._value = BooleanEventValue.NotSet; + } + + public setValue(_value: boolean) { + let value = (_value ? BooleanEventValue.True : BooleanEventValue.False); + if (this._value === value) { + return; + } + this._value = value; + if (this._value === BooleanEventValue.True) { + this._onDidChangeToTrue.fire(); + } else if (this._value === BooleanEventValue.False) { + this._onDidChangeToFalse.fire(); + } + } +} + class EditorContextKeysManager extends Disposable { private _editor: CommonCodeEditor; diff --git a/src/vs/editor/common/config/commonEditorConfig.ts b/src/vs/editor/common/config/commonEditorConfig.ts index 2435ca97847..62ebc3a17d9 100644 --- a/src/vs/editor/common/config/commonEditorConfig.ts +++ b/src/vs/editor/common/config/commonEditorConfig.ts @@ -211,7 +211,7 @@ const editorConfiguration: IConfigurationNode = { nls.localize('lineNumbers.interval', "Line numbers are rendered every 10 lines.") ], 'default': 'on', - 'description': nls.localize('lineNumbers', "Controls the display of line numbers. Possible values are 'on', 'off', 'relative' and 'interval'.") + 'description': nls.localize('lineNumbers', "Controls the display of line numbers.") }, 'editor.rulers': { 'type': 'array', @@ -268,13 +268,13 @@ const editorConfiguration: IConfigurationNode = { 'type': 'string', 'enum': ['left', 'right'], 'default': EDITOR_DEFAULTS.viewInfo.minimap.side, - 'description': nls.localize('minimap.side', "Controls the side where to render the minimap. Possible values are \'right\' and \'left\'") + 'description': nls.localize('minimap.side', "Controls the side where to render the minimap.") }, 'editor.minimap.showSlider': { 'type': 'string', 'enum': ['always', 'mouseover'], 'default': EDITOR_DEFAULTS.viewInfo.minimap.showSlider, - 'description': nls.localize('minimap.showSlider', "Controls whether the minimap slider is automatically hidden. Possible values are \'always\' and \'mouseover\'") + 'description': nls.localize('minimap.showSlider', "Controls whether the minimap slider is automatically hidden.") }, 'editor.minimap.renderCharacters': { 'type': 'boolean', @@ -370,6 +370,11 @@ const editorConfiguration: IConfigurationNode = { ] }, "The modifier to be used to add multiple cursors with the mouse. `ctrlCmd` maps to `Control` on Windows and Linux and to `Command` on macOS. The Go To Definition and Open Link mouse gestures will adapt such that they do not conflict with the multicursor modifier.") }, + 'editor.multiCursorMergeOverlapping': { + 'type': 'boolean', + 'default': EDITOR_DEFAULTS.multiCursorMergeOverlapping, + 'description': nls.localize('multiCursorMergeOverlapping', "Merge multiple cursors when they are overlapping.") + }, 'editor.quickSuggestions': { 'anyOf': [ { @@ -515,7 +520,7 @@ const editorConfiguration: IConfigurationNode = { 'type': 'string', 'enum': ['blink', 'smooth', 'phase', 'expand', 'solid'], 'default': editorOptions.blinkingStyleToString(EDITOR_DEFAULTS.viewInfo.cursorBlinking), - 'description': nls.localize('cursorBlinking', "Control the cursor animation style, possible values are 'blink', 'smooth', 'phase', 'expand' and 'solid'") + 'description': nls.localize('cursorBlinking', "Control the cursor animation style.") }, 'editor.mouseWheelZoom': { 'type': 'boolean', @@ -568,13 +573,23 @@ const editorConfiguration: IConfigurationNode = { 'editor.codeLens': { 'type': 'boolean', 'default': EDITOR_DEFAULTS.contribInfo.codeLens, - 'description': nls.localize('codeLens', "Controls if the editor shows code lenses") + 'description': nls.localize('codeLens', "Controls if the editor shows CodeLens") }, 'editor.folding': { 'type': 'boolean', 'default': EDITOR_DEFAULTS.contribInfo.folding, 'description': nls.localize('folding', "Controls whether the editor has code folding enabled") }, + 'editor.foldingStrategy': { + 'type': 'string', + 'enum': ['auto', 'indentation'], + 'enumDescriptions': [ + nls.localize('foldingStrategyAuto', 'If available, use a language specific folding strategy, otherwise falls back to the indentation based strategy.'), + nls.localize('foldingStrategyIndentation', 'Always use the indentation based folding strategy') + ], + 'default': EDITOR_DEFAULTS.contribInfo.foldingStrategy, + 'description': nls.localize('foldingStrategy', "Controls the way folding ranges are computed. 'auto' picks uses a language specific folding strategy, if available. 'indentation' forces that the indentation based folding strategy is used.") + }, 'editor.showFoldingControls': { 'type': 'string', 'enum': ['always', 'mouseover'], @@ -653,6 +668,16 @@ const editorConfiguration: IConfigurationNode = { 'default': true, 'description': nls.localize('ignoreTrimWhitespace', "Controls if the diff editor shows changes in leading or trailing whitespace as diffs") }, + 'editor.largeFileSize': { + 'type': 'number', + 'default': EDITOR_MODEL_DEFAULTS.largeFileSize, + 'description': nls.localize('largeFileSize', "Controls file size threshold in bytes beyond which special optimization rules are applied") + }, + 'editor.largeFileLineCount': { + 'type': 'number', + 'default': EDITOR_MODEL_DEFAULTS.largeFileLineCount, + 'description': nls.localize('largeFileLineCount', "Controls file size threshold in terms of line count beyond which special optimization rules are applied") + }, 'diffEditor.renderIndicators': { 'type': 'boolean', 'default': true, diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index 011cdb3dbf4..e4aa5cf65af 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -10,6 +10,7 @@ import { ScrollbarVisibility } from 'vs/base/common/scrollable'; import { FontInfo } from 'vs/editor/common/config/fontInfo'; import { Constants } from 'vs/editor/common/core/uint'; import { USUAL_WORD_SEPARATORS } from 'vs/editor/common/model/wordHelper'; +import * as arrays from 'vs/base/common/arrays'; /** * Configuration options for editor scrollbars @@ -381,6 +382,11 @@ export interface IEditorOptions { * Defaults to 'alt' */ multiCursorModifier?: 'ctrlCmd' | 'alt'; + /** + * Merge overlapping selections. + * Defaults to true + */ + multiCursorMergeOverlapping?: boolean; /** * Configure the editor's accessibility support. * Defaults to 'auto'. It is best to leave this to 'auto'. @@ -486,11 +492,6 @@ export interface IEditorOptions { * Defaults to true. */ codeLens?: boolean; - /** - * @deprecated - use codeLens instead - * @internal - */ - referenceInfos?: boolean; /** * Control the behavior and rendering of the code action lightbulb. */ @@ -500,6 +501,11 @@ export interface IEditorOptions { * Defaults to true. */ folding?: boolean; + /** + * Selects the folding strategy. 'auto' uses the strategies contributed for the current document, 'indentation' uses the indentation based folding strategy. + * Defaults to 'auto'. + */ + foldingStrategy?: 'auto' | 'indentation'; /** * Controls whether the fold actions in the gutter stay always visible or hide unless the mouse is over the gutter. * Defaults to 'mouseover'. @@ -838,6 +844,7 @@ export interface EditorContribOptions { readonly occurrencesHighlight: boolean; readonly codeLens: boolean; readonly folding: boolean; + readonly foldingStrategy: 'auto' | 'indentation'; readonly showFoldingControls: 'always' | 'mouseover'; readonly matchBrackets: boolean; readonly find: InternalEditorFindOptions; @@ -872,6 +879,7 @@ export interface IValidatedEditorOptions { readonly emptySelectionClipboard: boolean; readonly useTabStops: boolean; readonly multiCursorModifier: 'altKey' | 'ctrlKey' | 'metaKey'; + readonly multiCursorMergeOverlapping: boolean; readonly accessibilitySupport: 'auto' | 'off' | 'on'; readonly viewInfo: InternalEditorViewOptions; @@ -894,6 +902,7 @@ export class InternalEditorOptions { */ readonly accessibilitySupport: platform.AccessibilitySupport; readonly multiCursorModifier: 'altKey' | 'ctrlKey' | 'metaKey'; + readonly multiCursorMergeOverlapping: boolean; // ---- cursor options readonly wordSeparators: string; @@ -922,6 +931,7 @@ export class InternalEditorOptions { readOnly: boolean; accessibilitySupport: platform.AccessibilitySupport; multiCursorModifier: 'altKey' | 'ctrlKey' | 'metaKey'; + multiCursorMergeOverlapping: boolean; wordSeparators: string; autoClosingBrackets: boolean; autoIndent: boolean; @@ -942,6 +952,7 @@ export class InternalEditorOptions { this.readOnly = source.readOnly; this.accessibilitySupport = source.accessibilitySupport; this.multiCursorModifier = source.multiCursorModifier; + this.multiCursorMergeOverlapping = source.multiCursorMergeOverlapping; this.wordSeparators = source.wordSeparators; this.autoClosingBrackets = source.autoClosingBrackets; this.autoIndent = source.autoIndent; @@ -968,6 +979,7 @@ export class InternalEditorOptions { && this.readOnly === other.readOnly && this.accessibilitySupport === other.accessibilitySupport && this.multiCursorModifier === other.multiCursorModifier + && this.multiCursorMergeOverlapping === other.multiCursorMergeOverlapping && this.wordSeparators === other.wordSeparators && this.autoClosingBrackets === other.autoClosingBrackets && this.autoIndent === other.autoIndent @@ -995,6 +1007,7 @@ export class InternalEditorOptions { readOnly: (this.readOnly !== newOpts.readOnly), accessibilitySupport: (this.accessibilitySupport !== newOpts.accessibilitySupport), multiCursorModifier: (this.multiCursorModifier !== newOpts.multiCursorModifier), + multiCursorMergeOverlapping: (this.multiCursorMergeOverlapping !== newOpts.multiCursorMergeOverlapping), wordSeparators: (this.wordSeparators !== newOpts.wordSeparators), autoClosingBrackets: (this.autoClosingBrackets !== newOpts.autoClosingBrackets), autoIndent: (this.autoIndent !== newOpts.autoIndent), @@ -1058,7 +1071,7 @@ export class InternalEditorOptions { return ( a.extraEditorClassName === b.extraEditorClassName && a.disableMonospaceOptimizations === b.disableMonospaceOptimizations - && this._equalsNumberArrays(a.rulers, b.rulers) + && arrays.equals(a.rulers, b.rulers) && a.ariaLabel === b.ariaLabel && a.renderLineNumbers === b.renderLineNumbers && a.renderCustomLineNumbers === b.renderCustomLineNumbers @@ -1120,18 +1133,6 @@ export class InternalEditorOptions { ); } - private static _equalsNumberArrays(a: number[], b: number[]): boolean { - if (a.length !== b.length) { - return false; - } - for (let i = 0; i < a.length; i++) { - if (a[i] !== b[i]) { - return false; - } - } - return true; - } - /** * @internal */ @@ -1188,6 +1189,7 @@ export class InternalEditorOptions { && a.occurrencesHighlight === b.occurrencesHighlight && a.codeLens === b.codeLens && a.folding === b.folding + && a.foldingStrategy === b.foldingStrategy && a.showFoldingControls === b.showFoldingControls && a.matchBrackets === b.matchBrackets && this._equalFindOptions(a.find, b.find) @@ -1347,6 +1349,7 @@ export interface IConfigurationChangedEvent { readonly readOnly: boolean; readonly accessibilitySupport: boolean; readonly multiCursorModifier: boolean; + readonly multiCursorMergeOverlapping: boolean; readonly wordSeparators: boolean; readonly autoClosingBrackets: boolean; readonly autoIndent: boolean; @@ -1532,6 +1535,7 @@ export class EditorOptionsValidator { emptySelectionClipboard: _boolean(opts.emptySelectionClipboard, defaults.emptySelectionClipboard), useTabStops: _boolean(opts.useTabStops, defaults.useTabStops), multiCursorModifier: multiCursorModifier, + multiCursorMergeOverlapping: _boolean(opts.multiCursorMergeOverlapping, defaults.multiCursorMergeOverlapping), accessibilitySupport: _stringSet<'auto' | 'on' | 'off'>(opts.accessibilitySupport, defaults.accessibilitySupport, ['auto', 'on', 'off']), viewInfo: viewInfo, contribInfo: contribInfo, @@ -1652,7 +1656,11 @@ export class EditorOptionsValidator { renderLineHighlight = _stringSet<'none' | 'gutter' | 'line' | 'all'>(opts.renderLineHighlight, defaults.renderLineHighlight, ['none', 'gutter', 'line', 'all']); } - const mouseWheelScrollSensitivity = _float(opts.mouseWheelScrollSensitivity, defaults.scrollbar.mouseWheelScrollSensitivity); + let mouseWheelScrollSensitivity = _float(opts.mouseWheelScrollSensitivity, defaults.scrollbar.mouseWheelScrollSensitivity); + if (mouseWheelScrollSensitivity === 0) { + // Disallow 0, as it would prevent/block scrolling + mouseWheelScrollSensitivity = 1; + } const scrollbar = this._sanitizeScrollbarOpts(opts.scrollbar, defaults.scrollbar, mouseWheelScrollSensitivity); const minimap = this._sanitizeMinimapOpts(opts.minimap, defaults.minimap); @@ -1721,8 +1729,9 @@ export class EditorOptionsValidator { suggestLineHeight: _clampedInt(opts.suggestLineHeight, defaults.suggestLineHeight, 0, 1000), selectionHighlight: _boolean(opts.selectionHighlight, defaults.selectionHighlight), occurrencesHighlight: _boolean(opts.occurrencesHighlight, defaults.occurrencesHighlight), - codeLens: _boolean(opts.codeLens, defaults.codeLens) && _boolean(opts.referenceInfos, true), + codeLens: _boolean(opts.codeLens, defaults.codeLens), folding: _boolean(opts.folding, defaults.folding), + foldingStrategy: _stringSet<'auto' | 'indentation'>(opts.foldingStrategy, defaults.foldingStrategy, ['auto', 'indentation']), showFoldingControls: _stringSet<'always' | 'mouseover'>(opts.showFoldingControls, defaults.showFoldingControls, ['always', 'mouseover']), matchBrackets: _boolean(opts.matchBrackets, defaults.matchBrackets), find: find, @@ -1762,6 +1771,7 @@ export class InternalEditorOptionsFactory { emptySelectionClipboard: opts.emptySelectionClipboard, useTabStops: opts.useTabStops, multiCursorModifier: opts.multiCursorModifier, + multiCursorMergeOverlapping: opts.multiCursorMergeOverlapping, accessibilitySupport: opts.accessibilitySupport, viewInfo: { @@ -1824,6 +1834,7 @@ export class InternalEditorOptionsFactory { occurrencesHighlight: (accessibilityIsOn ? false : opts.contribInfo.occurrencesHighlight), // DISABLED WHEN SCREEN READER IS ATTACHED codeLens: (accessibilityIsOn ? false : opts.contribInfo.codeLens), // DISABLED WHEN SCREEN READER IS ATTACHED folding: (accessibilityIsOn ? false : opts.contribInfo.folding), // DISABLED WHEN SCREEN READER IS ATTACHED + foldingStrategy: opts.contribInfo.foldingStrategy, showFoldingControls: opts.contribInfo.showFoldingControls, matchBrackets: (accessibilityIsOn ? false : opts.contribInfo.matchBrackets), // DISABLED WHEN SCREEN READER IS ATTACHED find: opts.contribInfo.find, @@ -1968,6 +1979,7 @@ export class InternalEditorOptionsFactory { readOnly: opts.readOnly, accessibilitySupport: accessibilitySupport, multiCursorModifier: opts.multiCursorModifier, + multiCursorMergeOverlapping: opts.multiCursorMergeOverlapping, wordSeparators: opts.wordSeparators, autoClosingBrackets: opts.autoClosingBrackets, autoIndent: opts.autoIndent, @@ -2078,18 +2090,19 @@ export class EditorLayoutProvider { } // Given: - // viewportColumn = (contentWidth - verticalScrollbarWidth) / typicalHalfwidthCharacterWidth + // (leaving 2px for the cursor to have space after the last character) + // viewportColumn = (contentWidth - verticalScrollbarWidth - 2) / typicalHalfwidthCharacterWidth // minimapWidth = viewportColumn * minimapCharWidth // contentWidth = remainingWidth - minimapWidth // What are good values for contentWidth and minimapWidth ? - // minimapWidth = ((contentWidth - verticalScrollbarWidth) / typicalHalfwidthCharacterWidth) * minimapCharWidth - // typicalHalfwidthCharacterWidth * minimapWidth = (contentWidth - verticalScrollbarWidth) * minimapCharWidth - // typicalHalfwidthCharacterWidth * minimapWidth = (remainingWidth - minimapWidth - verticalScrollbarWidth) * minimapCharWidth - // (typicalHalfwidthCharacterWidth + minimapCharWidth) * minimapWidth = (remainingWidth - verticalScrollbarWidth) * minimapCharWidth - // minimapWidth = ((remainingWidth - verticalScrollbarWidth) * minimapCharWidth) / (typicalHalfwidthCharacterWidth + minimapCharWidth) + // minimapWidth = ((contentWidth - verticalScrollbarWidth - 2) / typicalHalfwidthCharacterWidth) * minimapCharWidth + // typicalHalfwidthCharacterWidth * minimapWidth = (contentWidth - verticalScrollbarWidth - 2) * minimapCharWidth + // typicalHalfwidthCharacterWidth * minimapWidth = (remainingWidth - minimapWidth - verticalScrollbarWidth - 2) * minimapCharWidth + // (typicalHalfwidthCharacterWidth + minimapCharWidth) * minimapWidth = (remainingWidth - verticalScrollbarWidth - 2) * minimapCharWidth + // minimapWidth = ((remainingWidth - verticalScrollbarWidth - 2) * minimapCharWidth) / (typicalHalfwidthCharacterWidth + minimapCharWidth) - minimapWidth = Math.max(0, Math.floor(((remainingWidth - verticalScrollbarWidth) * minimapCharWidth) / (typicalHalfwidthCharacterWidth + minimapCharWidth))); + minimapWidth = Math.max(0, Math.floor(((remainingWidth - verticalScrollbarWidth - 2) * minimapCharWidth) / (typicalHalfwidthCharacterWidth + minimapCharWidth))); let minimapColumns = minimapWidth / minimapCharWidth; if (minimapColumns > minimapMaxColumn) { minimapWidth = Math.floor(minimapMaxColumn * minimapCharWidth); @@ -2107,7 +2120,8 @@ export class EditorLayoutProvider { } } - const viewportColumn = Math.max(1, Math.floor((contentWidth - verticalScrollbarWidth) / typicalHalfwidthCharacterWidth)); + // (leaving 2px for the cursor to have space after the last character) + const viewportColumn = Math.max(1, Math.floor((contentWidth - verticalScrollbarWidth - 2) / typicalHalfwidthCharacterWidth)); const verticalArrowSize = (verticalScrollbarHasArrows ? scrollbarArrowSize : 0); @@ -2176,7 +2190,9 @@ export const EDITOR_MODEL_DEFAULTS = { tabSize: 4, insertSpaces: true, detectIndentation: true, - trimAutoWhitespace: true + trimAutoWhitespace: true, + largeFileSize: 20 * 1024 * 1024, // 20 MB + largeFileLineCount: 300 * 1000 // 300K lines }; /** @@ -2204,6 +2220,7 @@ export const EDITOR_DEFAULTS: IValidatedEditorOptions = { emptySelectionClipboard: true, useTabStops: true, multiCursorModifier: 'altKey', + multiCursorMergeOverlapping: true, accessibilitySupport: 'auto', viewInfo: { @@ -2279,6 +2296,7 @@ export const EDITOR_DEFAULTS: IValidatedEditorOptions = { occurrencesHighlight: true, codeLens: true, folding: true, + foldingStrategy: 'auto', showFoldingControls: 'mouseover', matchBrackets: true, find: { diff --git a/src/vs/editor/common/controller/cursor.ts b/src/vs/editor/common/controller/cursor.ts index 85045434b1d..36833531a0a 100644 --- a/src/vs/editor/common/controller/cursor.ts +++ b/src/vs/editor/common/controller/cursor.ts @@ -87,6 +87,14 @@ export class CursorModelState { export class Cursor extends viewEvents.ViewEventEmitter implements ICursors { + public static MAX_CURSOR_COUNT = 10000; + + private readonly _onDidReachMaxCursorCount: Emitter = this._register(new Emitter()); + public readonly onDidReachMaxCursorCount: Event = this._onDidReachMaxCursorCount.event; + + private readonly _onDidAttemptReadOnlyEdit: Emitter = this._register(new Emitter()); + public readonly onDidAttemptReadOnlyEdit: Event = this._onDidAttemptReadOnlyEdit.event; + private readonly _onDidChange: Emitter = this._register(new Emitter()); public readonly onDidChange: Event = this._onDidChange.event; @@ -185,6 +193,11 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors { } public setStates(source: string, reason: CursorChangeReason, states: CursorState[]): void { + if (states.length > Cursor.MAX_CURSOR_COUNT) { + states = states.slice(0, Cursor.MAX_CURSOR_COUNT); + this._onDidReachMaxCursorCount.fire(void 0); + } + const oldState = new CursorModelState(this._model, this); this._cursors.setStates(states); @@ -361,7 +374,7 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors { private _interpretCommandResult(cursorState: Selection[]): void { if (!cursorState || cursorState.length === 0) { - return; + cursorState = this._cursors.readSelectionFromMarkers(); } this._columnSelectData = null; @@ -456,6 +469,7 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors { if (this._configuration.editor.readOnly) { // All the remaining handlers will try to edit the model, // but we cannot edit when read only... + this._onDidAttemptReadOnlyEdit.fire(void 0); return; } diff --git a/src/vs/editor/common/controller/cursorCollection.ts b/src/vs/editor/common/controller/cursorCollection.ts index 9fcae512f2f..c50e7d3e4e6 100644 --- a/src/vs/editor/common/controller/cursorCollection.ts +++ b/src/vs/editor/common/controller/cursorCollection.ts @@ -193,6 +193,10 @@ export class CursorCollection { const currentViewSelection = current.viewSelection; const nextViewSelection = next.viewSelection; + if (!this.context.config.multiCursorMergeOverlapping) { + continue; + } + let shouldMergeCursors: boolean; if (nextViewSelection.isEmpty() || currentViewSelection.isEmpty()) { // Merge touching cursors if one of them is collapsed diff --git a/src/vs/editor/common/controller/cursorCommon.ts b/src/vs/editor/common/controller/cursorCommon.ts index 8c6c99f2d0c..8f0ff9075d2 100644 --- a/src/vs/editor/common/controller/cursorCommon.ts +++ b/src/vs/editor/common/controller/cursorCommon.ts @@ -78,6 +78,7 @@ export class CursorConfiguration { public readonly useTabStops: boolean; public readonly wordSeparators: string; public readonly emptySelectionClipboard: boolean; + public readonly multiCursorMergeOverlapping: boolean; public readonly autoClosingBrackets: boolean; public readonly autoIndent: boolean; public readonly autoClosingPairsOpen: CharacterMap; @@ -92,6 +93,7 @@ export class CursorConfiguration { e.layoutInfo || e.wordSeparators || e.emptySelectionClipboard + || e.multiCursorMergeOverlapping || e.autoClosingBrackets || e.useTabStops || e.lineHeight @@ -118,6 +120,7 @@ export class CursorConfiguration { this.useTabStops = c.useTabStops; this.wordSeparators = c.wordSeparators; this.emptySelectionClipboard = c.emptySelectionClipboard; + this.multiCursorMergeOverlapping = c.multiCursorMergeOverlapping; this.autoClosingBrackets = c.autoClosingBrackets; this.autoIndent = c.autoIndent; diff --git a/src/vs/editor/common/controller/cursorTypeOperations.ts b/src/vs/editor/common/controller/cursorTypeOperations.ts index 320f1f8b9c2..12077751e66 100644 --- a/src/vs/editor/common/controller/cursorTypeOperations.ts +++ b/src/vs/editor/common/controller/cursorTypeOperations.ts @@ -18,6 +18,7 @@ import { IndentAction, EnterAction } from 'vs/editor/common/modes/languageConfig import { SurroundSelectionCommand } from 'vs/editor/common/commands/surroundSelectionCommand'; import { IElectricAction } from 'vs/editor/common/modes/supports/electricCharacter'; import { getMapForWordSeparators, WordCharacterClass } from 'vs/editor/common/controller/wordCharacterClassifier'; +import { CharCode } from 'vs/base/common/charCode'; export class TypeOperations { @@ -123,6 +124,15 @@ export class TypeOperations { return multicursorText; } + // Remove trailing \n if present + if (text.charCodeAt(text.length - 1) === CharCode.LineFeed) { + text = text.substr(0, text.length - 1); + } + let lines = text.split(/\r\n|\r|\n/); + if (lines.length === selections.length) { + return lines; + } + return null; } diff --git a/src/vs/editor/common/controller/cursorWordOperations.ts b/src/vs/editor/common/controller/cursorWordOperations.ts index 8f83c1f8621..8bd73b791f0 100644 --- a/src/vs/editor/common/controller/cursorWordOperations.ts +++ b/src/vs/editor/common/controller/cursorWordOperations.ts @@ -24,6 +24,10 @@ interface IFindWordResult { * The word type. */ wordType: WordType; + /** + * The reason the word ended. + */ + nextCharClass: WordCharacterClass; } const enum WordType { @@ -39,9 +43,9 @@ export const enum WordNavigationType { export class WordOperations { - private static _createWord(lineContent: string, wordType: WordType, start: number, end: number): IFindWordResult { + private static _createWord(lineContent: string, wordType: WordType, nextCharClass: WordCharacterClass, start: number, end: number): IFindWordResult { // console.log('WORD ==> ' + start + ' => ' + end + ':::: <<<' + lineContent.substring(start, end) + '>>>'); - return { start: start, end: end, wordType: wordType }; + return { start: start, end: end, wordType: wordType, nextCharClass: nextCharClass }; } private static _findPreviousWordOnLine(wordSeparators: WordCharacterClassifier, model: ICursorSimpleModel, position: Position): IFindWordResult { @@ -57,23 +61,23 @@ export class WordOperations { if (chClass === WordCharacterClass.Regular) { if (wordType === WordType.Separator) { - return this._createWord(lineContent, wordType, chIndex + 1, this._findEndOfWord(lineContent, wordSeparators, wordType, chIndex + 1)); + return this._createWord(lineContent, wordType, chClass, chIndex + 1, this._findEndOfWord(lineContent, wordSeparators, wordType, chIndex + 1)); } wordType = WordType.Regular; } else if (chClass === WordCharacterClass.WordSeparator) { if (wordType === WordType.Regular) { - return this._createWord(lineContent, wordType, chIndex + 1, this._findEndOfWord(lineContent, wordSeparators, wordType, chIndex + 1)); + return this._createWord(lineContent, wordType, chClass, chIndex + 1, this._findEndOfWord(lineContent, wordSeparators, wordType, chIndex + 1)); } wordType = WordType.Separator; } else if (chClass === WordCharacterClass.Whitespace) { if (wordType !== WordType.None) { - return this._createWord(lineContent, wordType, chIndex + 1, this._findEndOfWord(lineContent, wordSeparators, wordType, chIndex + 1)); + return this._createWord(lineContent, wordType, chClass, chIndex + 1, this._findEndOfWord(lineContent, wordSeparators, wordType, chIndex + 1)); } } } if (wordType !== WordType.None) { - return this._createWord(lineContent, wordType, 0, this._findEndOfWord(lineContent, wordSeparators, wordType, 0)); + return this._createWord(lineContent, wordType, WordCharacterClass.Whitespace, 0, this._findEndOfWord(lineContent, wordSeparators, wordType, 0)); } return null; @@ -113,23 +117,23 @@ export class WordOperations { if (chClass === WordCharacterClass.Regular) { if (wordType === WordType.Separator) { - return this._createWord(lineContent, wordType, this._findStartOfWord(lineContent, wordSeparators, wordType, chIndex - 1), chIndex); + return this._createWord(lineContent, wordType, chClass, this._findStartOfWord(lineContent, wordSeparators, wordType, chIndex - 1), chIndex); } wordType = WordType.Regular; } else if (chClass === WordCharacterClass.WordSeparator) { if (wordType === WordType.Regular) { - return this._createWord(lineContent, wordType, this._findStartOfWord(lineContent, wordSeparators, wordType, chIndex - 1), chIndex); + return this._createWord(lineContent, wordType, chClass, this._findStartOfWord(lineContent, wordSeparators, wordType, chIndex - 1), chIndex); } wordType = WordType.Separator; } else if (chClass === WordCharacterClass.Whitespace) { if (wordType !== WordType.None) { - return this._createWord(lineContent, wordType, this._findStartOfWord(lineContent, wordSeparators, wordType, chIndex - 1), chIndex); + return this._createWord(lineContent, wordType, chClass, this._findStartOfWord(lineContent, wordSeparators, wordType, chIndex - 1), chIndex); } } } if (wordType !== WordType.None) { - return this._createWord(lineContent, wordType, this._findStartOfWord(lineContent, wordSeparators, wordType, len - 1), len); + return this._createWord(lineContent, wordType, WordCharacterClass.Whitespace, this._findStartOfWord(lineContent, wordSeparators, wordType, len - 1), len); } return null; @@ -167,6 +171,12 @@ export class WordOperations { let prevWordOnLine = WordOperations._findPreviousWordOnLine(wordSeparators, model, new Position(lineNumber, column)); if (wordNavigationType === WordNavigationType.WordStart) { + if (prevWordOnLine && prevWordOnLine.wordType === WordType.Separator) { + if (prevWordOnLine.end - prevWordOnLine.start === 1 && prevWordOnLine.nextCharClass === WordCharacterClass.Regular) { + // Skip over a word made up of one single separator and followed by a regular character + prevWordOnLine = WordOperations._findPreviousWordOnLine(wordSeparators, model, new Position(lineNumber, prevWordOnLine.start + 1)); + } + } if (prevWordOnLine) { column = prevWordOnLine.start + 1; } else { @@ -200,6 +210,12 @@ export class WordOperations { let nextWordOnLine = WordOperations._findNextWordOnLine(wordSeparators, model, new Position(lineNumber, column)); if (wordNavigationType === WordNavigationType.WordEnd) { + if (nextWordOnLine && nextWordOnLine.wordType === WordType.Separator) { + if (nextWordOnLine.end - nextWordOnLine.start === 1 && nextWordOnLine.nextCharClass === WordCharacterClass.Regular) { + // Skip over a word made up of one single separator and followed by a regular character + nextWordOnLine = WordOperations._findNextWordOnLine(wordSeparators, model, new Position(lineNumber, nextWordOnLine.end + 1)); + } + } if (nextWordOnLine) { column = nextWordOnLine.end + 1; } else { @@ -374,20 +390,20 @@ export class WordOperations { public static word(config: CursorConfiguration, model: ICursorSimpleModel, cursor: SingleCursorState, inSelectionMode: boolean, position: Position): SingleCursorState { const wordSeparators = getMapForWordSeparators(config.wordSeparators); let prevWord = WordOperations._findPreviousWordOnLine(wordSeparators, model, position); - let isInPrevWord = (prevWord && prevWord.wordType === WordType.Regular && prevWord.start < position.column - 1 && position.column - 1 <= prevWord.end); let nextWord = WordOperations._findNextWordOnLine(wordSeparators, model, position); - let isInNextWord = (nextWord && nextWord.wordType === WordType.Regular && nextWord.start < position.column - 1 && position.column - 1 <= nextWord.end); if (!inSelectionMode) { // Entering word selection for the first time + const isTouchingPrevWord = (prevWord && prevWord.wordType === WordType.Regular && prevWord.start <= position.column - 1 && position.column - 1 <= prevWord.end); + const isTouchingNextWord = (nextWord && nextWord.wordType === WordType.Regular && nextWord.start <= position.column - 1 && position.column - 1 <= nextWord.end); let startColumn: number; let endColumn: number; - if (isInPrevWord) { + if (isTouchingPrevWord) { startColumn = prevWord.start + 1; endColumn = prevWord.end + 1; - } else if (isInNextWord) { + } else if (isTouchingNextWord) { startColumn = nextWord.start + 1; endColumn = nextWord.end + 1; } else { @@ -409,13 +425,16 @@ export class WordOperations { ); } + const isInsidePrevWord = (prevWord && prevWord.wordType === WordType.Regular && prevWord.start < position.column - 1 && position.column - 1 < prevWord.end); + const isInsideNextWord = (nextWord && nextWord.wordType === WordType.Regular && nextWord.start < position.column - 1 && position.column - 1 < nextWord.end); + let startColumn: number; let endColumn: number; - if (isInPrevWord) { + if (isInsidePrevWord) { startColumn = prevWord.start + 1; endColumn = prevWord.end + 1; - } else if (isInNextWord) { + } else if (isInsideNextWord) { startColumn = nextWord.start + 1; endColumn = nextWord.end + 1; } else { diff --git a/src/vs/editor/common/controller/oneCursor.ts b/src/vs/editor/common/controller/oneCursor.ts index 30d6a1fb615..363cda5bb40 100644 --- a/src/vs/editor/common/controller/oneCursor.ts +++ b/src/vs/editor/common/controller/oneCursor.ts @@ -93,11 +93,6 @@ export class OneCursor { viewState = new SingleCursorState(viewSelectionStart, modelState.selectionStartLeftoverVisibleColumns, viewPosition, modelState.leftoverVisibleColumns); } - if (this.modelState && this.viewState && this.modelState.equals(modelState) && this.viewState.equals(viewState)) { - // No-op, early return - return; - } - this.modelState = modelState; this.viewState = viewState; diff --git a/src/vs/editor/common/diff/diffComputer.ts b/src/vs/editor/common/diff/diffComputer.ts index a1b88e19a4b..6bccc31a789 100644 --- a/src/vs/editor/common/diff/diffComputer.ts +++ b/src/vs/editor/common/diff/diffComputer.ts @@ -284,7 +284,7 @@ class LineChange implements ILineChange { const originalCharSequence = originalLineSequence.getCharSequence(diffChange.originalStart, diffChange.originalStart + diffChange.originalLength - 1); const modifiedCharSequence = modifiedLineSequence.getCharSequence(diffChange.modifiedStart, diffChange.modifiedStart + diffChange.modifiedLength - 1); - let rawChanges = computeDiff(originalCharSequence, modifiedCharSequence, continueProcessingPredicate, false); + let rawChanges = computeDiff(originalCharSequence, modifiedCharSequence, continueProcessingPredicate, true); if (shouldPostProcessCharChanges) { rawChanges = postProcessCharChanges(rawChanges); diff --git a/src/vs/editor/common/editorCommon.ts b/src/vs/editor/common/editorCommon.ts index 44106c97324..8be479866df 100644 --- a/src/vs/editor/common/editorCommon.ts +++ b/src/vs/editor/common/editorCommon.ts @@ -199,9 +199,13 @@ export interface ICursorState { * A (serializable) state of the view. */ export interface IViewState { - scrollTop: number; - scrollTopWithoutViewZones: number; + /** written by previous versions */ + scrollTop?: number; + /** written by previous versions */ + scrollTopWithoutViewZones?: number; scrollLeft: number; + firstPosition: IPosition; + firstPositionDeltaTop: number; } /** * A (serializable) state of the code editor. @@ -505,6 +509,7 @@ export interface IThemeDecorationRenderOptions { textDecoration?: string; cursor?: string; color?: string | ThemeColor; + opacity?: number; letterSpacing?: string; gutterIconPath?: string | UriComponents; diff --git a/src/vs/editor/common/model.ts b/src/vs/editor/common/model.ts index 4dc552e92d2..3f628c4136a 100644 --- a/src/vs/editor/common/model.ts +++ b/src/vs/editor/common/model.ts @@ -81,7 +81,12 @@ export interface IModelDecorationOptions { * Always render the decoration (even when the range it encompasses is collapsed). * @internal */ - readonly showIfCollapsed?: boolean; + showIfCollapsed?: boolean; + /** + * Specifies the stack order of a decoration. + * A decoration with greater stack order is always in front of a decoration with a lower stack order. + */ + zIndex?: number; /** * If set, render this decoration in the overview ruler. */ @@ -104,6 +109,10 @@ export interface IModelDecorationOptions { * to have a background color decoration. */ inlineClassName?: string; + /** + * If there is an `inlineClassName` which affects letter spacing. + */ + inlineClassNameAffectsLetterSpacing?: boolean; /** * If set, the decoration will be rendered before the text with this CSS class name. */ @@ -391,6 +400,8 @@ export interface ITextModelCreationOptions { trimAutoWhitespace: boolean; defaultEOL: DefaultEndOfLine; isForSimpleWidget: boolean; + largeFileSize: number; + largeFileLineCount: number; } export interface ITextModelUpdateOptions { @@ -435,6 +446,15 @@ export enum TrackedRangeStickiness { GrowsOnlyWhenTypingAfter = 3, } +/** + * @internal + */ +export interface IActiveIndentGuideInfo { + startLineNumber: number; + endLineNumber: number; + indent: number; +} + /** * A model. */ @@ -561,6 +581,10 @@ export interface ITextModel { */ getLineContent(lineNumber: number): string; + /** + * Get the text length for a certain line. + */ + getLineLength(lineNumber: number): number; /** * Get the text for all lines. @@ -842,6 +866,11 @@ export interface ITextModel { */ matchBracket(position: IPosition): [Range, Range]; + /** + * @internal + */ + getActiveIndentGuide(lineNumber: number): IActiveIndentGuideInfo; + /** * @internal */ @@ -1002,6 +1031,13 @@ export interface ITextModel { */ redo(): Selection[]; + /** + * @deprecated Please use `onDidChangeContent` instead. + * An event emitted when the contents of the model have changed. + * @internal + * @event + */ + onDidChangeRawContentFast(listener: (e: ModelRawContentChangedEvent) => void): IDisposable; /** * @deprecated Please use `onDidChangeContent` instead. * An event emitted when the contents of the model have changed. @@ -1067,6 +1103,12 @@ export interface ITextModel { * @internal */ isAttachedToEditor(): boolean; + + /** + * Returns the count of editors this model is attached to. + * @internal + */ + getAttachedEditorCount(): number; } /** @@ -1134,6 +1176,5 @@ export class ApplyEditsResult { */ export interface IInternalModelContentChange extends IModelContentChange { range: Range; - rangeOffset: number; forceMoveMarkers: boolean; } diff --git a/src/vs/editor/common/model/chunksTextBuffer/bufferPiece.ts b/src/vs/editor/common/model/chunksTextBuffer/bufferPiece.ts deleted file mode 100644 index 53063409aea..00000000000 --- a/src/vs/editor/common/model/chunksTextBuffer/bufferPiece.ts +++ /dev/null @@ -1,323 +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 { CharCode } from 'vs/base/common/charCode'; - -export class LeafOffsetLenEdit { - constructor( - public readonly start: number, - public readonly length: number, - public readonly text: string - ) { } -} - -export class BufferPiece { - private readonly _str: string; - public get text(): string { return this._str; } - - private readonly _lineStarts: Uint32Array; - - constructor(str: string, lineStarts: Uint32Array = null) { - this._str = str; - if (lineStarts === null) { - this._lineStarts = createLineStartsFast(str); - } else { - this._lineStarts = lineStarts; - } - } - - public length(): number { - return this._str.length; - } - - public newLineCount(): number { - return this._lineStarts.length; - } - - public lineStartFor(relativeLineIndex: number): number { - return this._lineStarts[relativeLineIndex]; - } - - public charCodeAt(index: number): number { - return this._str.charCodeAt(index); - } - - public substr(from: number, length: number): string { - return this._str.substr(from, length); - } - - public findLineStartBeforeOffset(offset: number): number { - if (this._lineStarts.length === 0 || offset < this._lineStarts[0]) { - return -1; - } - - let low = 0, high = this._lineStarts.length - 1; - - while (low < high) { - let mid = low + Math.ceil((high - low) / 2); - let lineStart = this._lineStarts[mid]; - - if (offset === lineStart) { - return mid; - } else if (offset < lineStart) { - high = mid - 1; - } else { - low = mid; - } - } - - return low; - } - - public findLineFirstNonWhitespaceIndex(searchStartOffset: number): number { - for (let i = searchStartOffset, len = this._str.length; i < len; i++) { - const chCode = this._str.charCodeAt(i); - if (chCode === CharCode.CarriageReturn || chCode === CharCode.LineFeed) { - // Reached EOL - return -2; - } - if (chCode !== CharCode.Space && chCode !== CharCode.Tab) { - return i; - } - } - return -1; - } - - public findLineLastNonWhitespaceIndex(searchStartOffset: number): number { - for (let i = searchStartOffset - 1; i >= 0; i--) { - const chCode = this._str.charCodeAt(i); - if (chCode === CharCode.CarriageReturn || chCode === CharCode.LineFeed) { - // Reached EOL - return -2; - } - if (chCode !== CharCode.Space && chCode !== CharCode.Tab) { - return i; - } - } - return -1; - } - - public static normalizeEOL(target: BufferPiece, eol: '\r\n' | '\n'): BufferPiece { - return new BufferPiece(target._str.replace(/\r\n|\r|\n/g, eol)); - } - - public static deleteLastChar(target: BufferPiece): BufferPiece { - const targetCharsLength = target.length(); - const targetLineStartsLength = target.newLineCount(); - const targetLineStarts = target._lineStarts; - - let newLineStartsLength; - if (targetLineStartsLength > 0 && targetLineStarts[targetLineStartsLength - 1] === targetCharsLength) { - newLineStartsLength = targetLineStartsLength - 1; - } else { - newLineStartsLength = targetLineStartsLength; - } - - let newLineStarts = new Uint32Array(newLineStartsLength); - newLineStarts.set(targetLineStarts); - - return new BufferPiece( - target._str.substr(0, targetCharsLength - 1), - newLineStarts - ); - } - - public static insertFirstChar(target: BufferPiece, character: number): BufferPiece { - const targetLineStartsLength = target.newLineCount(); - const targetLineStarts = target._lineStarts; - const insertLineStart = ((character === CharCode.CarriageReturn && (targetLineStartsLength === 0 || targetLineStarts[0] !== 1 || target.charCodeAt(0) !== CharCode.LineFeed)) || (character === CharCode.LineFeed)); - - const newLineStartsLength = (insertLineStart ? targetLineStartsLength + 1 : targetLineStartsLength); - let newLineStarts = new Uint32Array(newLineStartsLength); - - if (insertLineStart) { - newLineStarts[0] = 1; - for (let i = 0; i < targetLineStartsLength; i++) { - newLineStarts[i + 1] = targetLineStarts[i] + 1; - } - } else { - for (let i = 0; i < targetLineStartsLength; i++) { - newLineStarts[i] = targetLineStarts[i] + 1; - } - } - - return new BufferPiece( - String.fromCharCode(character) + target._str, - newLineStarts - ); - } - - public static join(first: BufferPiece, second: BufferPiece): BufferPiece { - const firstCharsLength = first._str.length; - - const firstLineStartsLength = first._lineStarts.length; - const secondLineStartsLength = second._lineStarts.length; - - const firstLineStarts = first._lineStarts; - const secondLineStarts = second._lineStarts; - - const newLineStartsLength = firstLineStartsLength + secondLineStartsLength; - let newLineStarts = new Uint32Array(newLineStartsLength); - newLineStarts.set(firstLineStarts, 0); - for (let i = 0; i < secondLineStartsLength; i++) { - newLineStarts[i + firstLineStartsLength] = secondLineStarts[i] + firstCharsLength; - } - - return new BufferPiece(first._str + second._str, newLineStarts); - } - - public static replaceOffsetLen(target: BufferPiece, edits: LeafOffsetLenEdit[], idealLeafLength: number, maxLeafLength: number, result: BufferPiece[]): void { - const editsSize = edits.length; - const originalCharsLength = target.length(); - if (editsSize === 1 && edits[0].text.length === 0 && edits[0].start === 0 && edits[0].length === originalCharsLength) { - // special case => deleting everything - return; - } - - let pieces: string[] = new Array(2 * editsSize + 1); - let originalFromIndex = 0; - let piecesTextLength = 0; - for (let i = 0; i < editsSize; i++) { - const edit = edits[i]; - - const originalText = target._str.substr(originalFromIndex, edit.start - originalFromIndex); - pieces[2 * i] = originalText; - piecesTextLength += originalText.length; - - originalFromIndex = edit.start + edit.length; - pieces[2 * i + 1] = edit.text; - piecesTextLength += edit.text.length; - } - - // maintain the chars that survive to the right of the last edit - let text = target._str.substr(originalFromIndex, originalCharsLength - originalFromIndex); - pieces[2 * editsSize] = text; - piecesTextLength += text.length; - - let targetDataLength = piecesTextLength > maxLeafLength ? idealLeafLength : piecesTextLength; - let targetDataOffset = 0; - - let data: string = ''; - - for (let pieceIndex = 0, pieceCount = pieces.length; pieceIndex < pieceCount; pieceIndex++) { - const pieceText = pieces[pieceIndex]; - const pieceLength = pieceText.length; - if (pieceLength === 0) { - continue; - } - - let pieceOffset = 0; - while (pieceOffset < pieceLength) { - if (targetDataOffset >= targetDataLength) { - result.push(new BufferPiece(data)); - targetDataLength = piecesTextLength > maxLeafLength ? idealLeafLength : piecesTextLength; - targetDataOffset = 0; - data = ''; - } - - let writingCnt = min(pieceLength - pieceOffset, targetDataLength - targetDataOffset); - data += pieceText.substr(pieceOffset, writingCnt); - pieceOffset += writingCnt; - targetDataOffset += writingCnt; - piecesTextLength -= writingCnt; - - // check that the buffer piece does not end in a \r or high surrogate - if (targetDataOffset === targetDataLength && piecesTextLength > 0) { - const lastChar = data.charCodeAt(targetDataLength - 1); - if (lastChar === CharCode.CarriageReturn || (0xD800 <= lastChar && lastChar <= 0xDBFF)) { - // move lastChar over to next buffer piece - targetDataLength -= 1; - pieceOffset -= 1; - targetDataOffset -= 1; - piecesTextLength += 1; - data = data.substr(0, data.length - 1); - } - } - } - } - - result.push(new BufferPiece(data)); - } -} - -function min(a: number, b: number): number { - return (a < b ? a : b); -} - -export function createUint32Array(arr: number[]): Uint32Array { - let r = new Uint32Array(arr.length); - r.set(arr, 0); - return r; -} - -export class LineStarts { - constructor( - public readonly lineStarts: Uint32Array, - public readonly cr: number, - public readonly lf: number, - public readonly crlf: number, - public readonly isBasicASCII: boolean - ) { } -} - -export function createLineStartsFast(str: string): Uint32Array { - let r: number[] = [], rLength = 0; - for (let i = 0, len = str.length; i < len; i++) { - const chr = str.charCodeAt(i); - - if (chr === CharCode.CarriageReturn) { - if (i + 1 < len && str.charCodeAt(i + 1) === CharCode.LineFeed) { - // \r\n... case - r[rLength++] = i + 2; - i++; // skip \n - } else { - // \r... case - r[rLength++] = i + 1; - } - } else if (chr === CharCode.LineFeed) { - r[rLength++] = i + 1; - } - } - return createUint32Array(r); -} - -export function createLineStarts(r: number[], str: string): LineStarts { - r.length = 0; - - let rLength = 0; - let cr = 0, lf = 0, crlf = 0; - let isBasicASCII = true; - for (let i = 0, len = str.length; i < len; i++) { - const chr = str.charCodeAt(i); - - if (chr === CharCode.CarriageReturn) { - if (i + 1 < len && str.charCodeAt(i + 1) === CharCode.LineFeed) { - // \r\n... case - crlf++; - r[rLength++] = i + 2; - i++; // skip \n - } else { - cr++; - // \r... case - r[rLength++] = i + 1; - } - } else if (chr === CharCode.LineFeed) { - lf++; - r[rLength++] = i + 1; - } else { - if (isBasicASCII) { - if (chr !== CharCode.Tab && (chr < 32 || chr > 126)) { - isBasicASCII = false; - } - } - } - } - - const result = new LineStarts(createUint32Array(r), cr, lf, crlf, isBasicASCII); - r.length = 0; - - return result; -} diff --git a/src/vs/editor/common/model/chunksTextBuffer/chunksTextBuffer.ts b/src/vs/editor/common/model/chunksTextBuffer/chunksTextBuffer.ts deleted file mode 100644 index 046f3387ea5..00000000000 --- a/src/vs/editor/common/model/chunksTextBuffer/chunksTextBuffer.ts +++ /dev/null @@ -1,1526 +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 { CharCode } from 'vs/base/common/charCode'; -import { ITextBuffer, EndOfLinePreference, IIdentifiedSingleEditOperation, ApplyEditsResult, ISingleEditOperationIdentifier, IInternalModelContentChange } from 'vs/editor/common/model'; -import { BufferPiece, LeafOffsetLenEdit } from 'vs/editor/common/model/chunksTextBuffer/bufferPiece'; -import { Position } from 'vs/editor/common/core/position'; -import { Range } from 'vs/editor/common/core/range'; -import * as strings from 'vs/base/common/strings'; -import { ITextSnapshot } from 'vs/platform/files/common/files'; - -export interface IValidatedEditOperation { - sortIndex: number; - identifier: ISingleEditOperationIdentifier; - range: Range; - rangeOffset: number; - rangeLength: number; - lines: string[]; - forceMoveMarkers: boolean; - isAutoWhitespaceEdit: boolean; -} - -export class ChunksTextBuffer implements ITextBuffer { - - private _BOM: string; - private _actual: Buffer; - private _mightContainRTL: boolean; - private _mightContainNonBasicASCII: boolean; - - constructor(pieces: BufferPiece[], _averageChunkSize: number, BOM: string, eol: '\r\n' | '\n', containsRTL: boolean, isBasicASCII: boolean) { - this._BOM = BOM; - const averageChunkSize = Math.floor(Math.min(65536.0, Math.max(128.0, _averageChunkSize))); - const delta = Math.floor(averageChunkSize / 3); - const min = averageChunkSize - delta; - const max = 2 * min; - this._actual = new Buffer(pieces, min, max, eol); - this._mightContainRTL = containsRTL; - this._mightContainNonBasicASCII = !isBasicASCII; - } - - equals(other: ITextBuffer): boolean { - if (!(other instanceof ChunksTextBuffer)) { - return false; - } - return this._actual.equals(other._actual); - } - mightContainRTL(): boolean { - return this._mightContainRTL; - } - mightContainNonBasicASCII(): boolean { - return this._mightContainNonBasicASCII; - } - getBOM(): string { - return this._BOM; - } - getEOL(): string { - return this._actual.getEOL(); - } - getOffsetAt(lineNumber: number, column: number): number { - return this._actual.convertPositionToOffset(lineNumber, column); - } - getPositionAt(offset: number): Position { - return this._actual.convertOffsetToPosition(offset); - } - getRangeAt(offset: number, length: number): Range { - return this._actual.convertOffsetLenToRange(offset, length); - } - getValueInRange(range: Range, eol: EndOfLinePreference): string { - if (range.isEmpty()) { - return ''; - } - - const text = this._actual.getValueInRange(range); - switch (eol) { - case EndOfLinePreference.TextDefined: - return text; - case EndOfLinePreference.LF: - if (this.getEOL() === '\n') { - return text; - } else { - return text.replace(/\r\n/g, '\n'); - } - case EndOfLinePreference.CRLF: - if (this.getEOL() === '\r\n') { - return text; - } else { - return text.replace(/\n/g, '\r\n'); - } - } - return null; - } - - public createSnapshot(preserveBOM: boolean): ITextSnapshot { - return this._actual.createSnapshot(preserveBOM ? this._BOM : ''); - } - - getValueLengthInRange(range: Range, eol: EndOfLinePreference): number { - if (range.isEmpty()) { - return 0; - } - const eolCount = range.endLineNumber - range.startLineNumber; - const result = this._actual.getValueLengthInRange(range); - switch (eol) { - case EndOfLinePreference.TextDefined: - return result; - case EndOfLinePreference.LF: - if (this.getEOL() === '\n') { - return result; - } else { - return result - eolCount; // \r\n => \n - } - case EndOfLinePreference.CRLF: - if (this.getEOL() === '\r\n') { - return result; - } else { - return result + eolCount; // \n => \r\n - } - } - return 0; - } - - public getLength(): number { - return this._actual.getLength(); - } - - getLineCount(): number { - return this._actual.getLineCount(); - } - - getLinesContent(): string[] { - return this._actual.getLinesContent(); - } - - getLineContent(lineNumber: number): string { - return this._actual.getLineContent(lineNumber); - } - - getLineCharCode(lineNumber: number, index: number): number { - return this._actual.getLineCharCode(lineNumber, index); - } - - getLineLength(lineNumber: number): number { - return this._actual.getLineLength(lineNumber); - } - - getLineFirstNonWhitespaceColumn(lineNumber: number): number { - const result = this._actual.getLineFirstNonWhitespaceIndex(lineNumber); - if (result === -1) { - return 0; - } - return result + 1; - } - getLineLastNonWhitespaceColumn(lineNumber: number): number { - const result = this._actual.getLineLastNonWhitespaceIndex(lineNumber); - if (result === -1) { - return 0; - } - return result + 1; - } - setEOL(newEOL: '\r\n' | '\n'): void { - if (this.getEOL() === newEOL) { - // nothing to do... - return; - } - this._actual.setEOL(newEOL); - } - - private static _sortOpsAscending(a: IValidatedEditOperation, b: IValidatedEditOperation): number { - let r = Range.compareRangesUsingEnds(a.range, b.range); - if (r === 0) { - return a.sortIndex - b.sortIndex; - } - return r; - } - - private static _sortOpsDescending(a: IValidatedEditOperation, b: IValidatedEditOperation): number { - let r = Range.compareRangesUsingEnds(a.range, b.range); - if (r === 0) { - return b.sortIndex - a.sortIndex; - } - return -r; - } - - applyEdits(rawOperations: IIdentifiedSingleEditOperation[], recordTrimAutoWhitespace: boolean): ApplyEditsResult { - if (rawOperations.length === 0) { - return new ApplyEditsResult([], [], []); - } - - let mightContainRTL = this._mightContainRTL; - let mightContainNonBasicASCII = this._mightContainNonBasicASCII; - let canReduceOperations = true; - - let operations: IValidatedEditOperation[] = []; - for (let i = 0; i < rawOperations.length; i++) { - let op = rawOperations[i]; - if (canReduceOperations && op._isTracked) { - canReduceOperations = false; - } - let validatedRange = op.range; - if (!mightContainRTL && op.text) { - // check if the new inserted text contains RTL - mightContainRTL = strings.containsRTL(op.text); - } - if (!mightContainNonBasicASCII && op.text) { - mightContainNonBasicASCII = !strings.isBasicASCII(op.text); - } - operations[i] = { - sortIndex: i, - identifier: op.identifier || null, - range: validatedRange, - rangeOffset: this.getOffsetAt(validatedRange.startLineNumber, validatedRange.startColumn), - rangeLength: this.getValueLengthInRange(validatedRange, EndOfLinePreference.TextDefined), - lines: op.text ? op.text.split(/\r\n|\r|\n/) : null, - forceMoveMarkers: op.forceMoveMarkers || false, - isAutoWhitespaceEdit: op.isAutoWhitespaceEdit || false - }; - } - - // Sort operations ascending - operations.sort(ChunksTextBuffer._sortOpsAscending); - - for (let i = 0, count = operations.length - 1; i < count; i++) { - let rangeEnd = operations[i].range.getEndPosition(); - let nextRangeStart = operations[i + 1].range.getStartPosition(); - - if (nextRangeStart.isBefore(rangeEnd)) { - // overlapping ranges - throw new Error('Overlapping ranges are not allowed!'); - } - } - - if (canReduceOperations) { - operations = this._reduceOperations(operations); - } - - // Delta encode operations - let reverseRanges = ChunksTextBuffer._getInverseEditRanges(operations); - let newTrimAutoWhitespaceCandidates: { lineNumber: number, oldContent: string }[] = []; - - for (let i = 0; i < operations.length; i++) { - let op = operations[i]; - let reverseRange = reverseRanges[i]; - - if (recordTrimAutoWhitespace && op.isAutoWhitespaceEdit && op.range.isEmpty()) { - // Record already the future line numbers that might be auto whitespace removal candidates on next edit - for (let lineNumber = reverseRange.startLineNumber; lineNumber <= reverseRange.endLineNumber; lineNumber++) { - let currentLineContent = ''; - if (lineNumber === reverseRange.startLineNumber) { - currentLineContent = this.getLineContent(op.range.startLineNumber); - if (strings.firstNonWhitespaceIndex(currentLineContent) !== -1) { - continue; - } - } - newTrimAutoWhitespaceCandidates.push({ lineNumber: lineNumber, oldContent: currentLineContent }); - } - } - } - - let reverseOperations: IIdentifiedSingleEditOperation[] = []; - for (let i = 0; i < operations.length; i++) { - let op = operations[i]; - let reverseRange = reverseRanges[i]; - - reverseOperations[i] = { - identifier: op.identifier, - range: reverseRange, - text: this.getValueInRange(op.range, EndOfLinePreference.TextDefined), - forceMoveMarkers: op.forceMoveMarkers - }; - } - - this._mightContainRTL = mightContainRTL; - this._mightContainNonBasicASCII = mightContainNonBasicASCII; - - const contentChanges = this._doApplyEdits(operations); - - let trimAutoWhitespaceLineNumbers: number[] = null; - if (recordTrimAutoWhitespace && newTrimAutoWhitespaceCandidates.length > 0) { - // sort line numbers auto whitespace removal candidates for next edit descending - newTrimAutoWhitespaceCandidates.sort((a, b) => b.lineNumber - a.lineNumber); - - trimAutoWhitespaceLineNumbers = []; - for (let i = 0, len = newTrimAutoWhitespaceCandidates.length; i < len; i++) { - let lineNumber = newTrimAutoWhitespaceCandidates[i].lineNumber; - if (i > 0 && newTrimAutoWhitespaceCandidates[i - 1].lineNumber === lineNumber) { - // Do not have the same line number twice - continue; - } - - let prevContent = newTrimAutoWhitespaceCandidates[i].oldContent; - let lineContent = this.getLineContent(lineNumber); - - if (lineContent.length === 0 || lineContent === prevContent || strings.firstNonWhitespaceIndex(lineContent) !== -1) { - continue; - } - - trimAutoWhitespaceLineNumbers.push(lineNumber); - } - } - - return new ApplyEditsResult( - reverseOperations, - contentChanges, - trimAutoWhitespaceLineNumbers - ); - } - - /** - * Transform operations such that they represent the same logic edit, - * but that they also do not cause OOM crashes. - */ - private _reduceOperations(operations: IValidatedEditOperation[]): IValidatedEditOperation[] { - if (operations.length < 1000) { - // We know from empirical testing that a thousand edits work fine regardless of their shape. - return operations; - } - - // At one point, due to how events are emitted and how each operation is handled, - // some operations can trigger a high ammount of temporary string allocations, - // that will immediately get edited again. - // e.g. a formatter inserting ridiculous ammounts of \n on a model with a single line - // Therefore, the strategy is to collapse all the operations into a huge single edit operation - return [this._toSingleEditOperation(operations)]; - } - - _toSingleEditOperation(operations: IValidatedEditOperation[]): IValidatedEditOperation { - let forceMoveMarkers = false, - firstEditRange = operations[0].range, - lastEditRange = operations[operations.length - 1].range, - entireEditRange = new Range(firstEditRange.startLineNumber, firstEditRange.startColumn, lastEditRange.endLineNumber, lastEditRange.endColumn), - lastEndLineNumber = firstEditRange.startLineNumber, - lastEndColumn = firstEditRange.startColumn, - result: string[] = []; - - for (let i = 0, len = operations.length; i < len; i++) { - let operation = operations[i], - range = operation.range; - - forceMoveMarkers = forceMoveMarkers || operation.forceMoveMarkers; - - // (1) -- Push old text - for (let lineNumber = lastEndLineNumber; lineNumber < range.startLineNumber; lineNumber++) { - if (lineNumber === lastEndLineNumber) { - result.push(this.getLineContent(lineNumber).substring(lastEndColumn - 1)); - } else { - result.push('\n'); - result.push(this.getLineContent(lineNumber)); - } - } - - if (range.startLineNumber === lastEndLineNumber) { - result.push(this.getLineContent(range.startLineNumber).substring(lastEndColumn - 1, range.startColumn - 1)); - } else { - result.push('\n'); - result.push(this.getLineContent(range.startLineNumber).substring(0, range.startColumn - 1)); - } - - // (2) -- Push new text - if (operation.lines) { - for (let j = 0, lenJ = operation.lines.length; j < lenJ; j++) { - if (j !== 0) { - result.push('\n'); - } - result.push(operation.lines[j]); - } - } - - lastEndLineNumber = operation.range.endLineNumber; - lastEndColumn = operation.range.endColumn; - } - - return { - sortIndex: 0, - identifier: operations[0].identifier, - range: entireEditRange, - rangeOffset: this.getOffsetAt(entireEditRange.startLineNumber, entireEditRange.startColumn), - rangeLength: this.getValueLengthInRange(entireEditRange, EndOfLinePreference.TextDefined), - lines: result.join('').split('\n'), - forceMoveMarkers: forceMoveMarkers, - isAutoWhitespaceEdit: false - }; - } - - private _doApplyEdits(operations: IValidatedEditOperation[]): IInternalModelContentChange[] { - - // Sort operations descending - operations.sort(ChunksTextBuffer._sortOpsDescending); - - let contentChanges: IInternalModelContentChange[] = []; - let edits: OffsetLenEdit[] = []; - - for (let i = 0, len = operations.length; i < len; i++) { - const op = operations[i]; - - const text = (op.lines ? op.lines.join(this.getEOL()) : ''); - edits[i] = new OffsetLenEdit(op.sortIndex, op.rangeOffset, op.rangeLength, text); - - const startLineNumber = op.range.startLineNumber; - const startColumn = op.range.startColumn; - const endLineNumber = op.range.endLineNumber; - const endColumn = op.range.endColumn; - - if (startLineNumber === endLineNumber && startColumn === endColumn && (!op.lines || op.lines.length === 0)) { - // no-op - continue; - } - - contentChanges.push({ - range: op.range, - rangeLength: op.rangeLength, - text: text, - rangeOffset: op.rangeOffset, - forceMoveMarkers: op.forceMoveMarkers - }); - } - - this._actual.replaceOffsetLen(edits); - - return contentChanges; - } - - /** - * Assumes `operations` are validated and sorted ascending - */ - public static _getInverseEditRanges(operations: IValidatedEditOperation[]): Range[] { - let result: Range[] = []; - - let prevOpEndLineNumber: number; - let prevOpEndColumn: number; - let prevOp: IValidatedEditOperation = null; - for (let i = 0, len = operations.length; i < len; i++) { - let op = operations[i]; - - let startLineNumber: number; - let startColumn: number; - - if (prevOp) { - if (prevOp.range.endLineNumber === op.range.startLineNumber) { - startLineNumber = prevOpEndLineNumber; - startColumn = prevOpEndColumn + (op.range.startColumn - prevOp.range.endColumn); - } else { - startLineNumber = prevOpEndLineNumber + (op.range.startLineNumber - prevOp.range.endLineNumber); - startColumn = op.range.startColumn; - } - } else { - startLineNumber = op.range.startLineNumber; - startColumn = op.range.startColumn; - } - - let resultRange: Range; - - if (op.lines && op.lines.length > 0) { - // the operation inserts something - let lineCount = op.lines.length; - let firstLine = op.lines[0]; - let lastLine = op.lines[lineCount - 1]; - - if (lineCount === 1) { - // single line insert - resultRange = new Range(startLineNumber, startColumn, startLineNumber, startColumn + firstLine.length); - } else { - // multi line insert - resultRange = new Range(startLineNumber, startColumn, startLineNumber + lineCount - 1, lastLine.length + 1); - } - } else { - // There is nothing to insert - resultRange = new Range(startLineNumber, startColumn, startLineNumber, startColumn); - } - - prevOpEndLineNumber = resultRange.endLineNumber; - prevOpEndColumn = resultRange.endColumn; - - result.push(resultRange); - prevOp = op; - } - - return result; - } -} - - -class BufferNodes { - - public length: Uint32Array; - public newLineCount: Uint32Array; - - constructor(count: number) { - this.length = new Uint32Array(count); - this.newLineCount = new Uint32Array(count); - } - -} - -class BufferCursor { - constructor( - public offset: number, - public leafIndex: number, - public leafStartOffset: number, - public leafStartNewLineCount: number - ) { } - - public set(offset: number, leafIndex: number, leafStartOffset: number, leafStartNewLineCount: number) { - this.offset = offset; - this.leafIndex = leafIndex; - this.leafStartOffset = leafStartOffset; - this.leafStartNewLineCount = leafStartNewLineCount; - } -} - -class OffsetLenEdit { - constructor( - public readonly initialIndex: number, - public readonly offset: number, - public length: number, - public text: string - ) { } -} - -class InternalOffsetLenEdit { - constructor( - public readonly startLeafIndex: number, - public readonly startInnerOffset: number, - public readonly endLeafIndex: number, - public readonly endInnerOffset: number, - public text: string - ) { } -} - -class LeafReplacement { - constructor( - public readonly startLeafIndex: number, - public readonly endLeafIndex: number, - public readonly replacements: BufferPiece[] - ) { } -} - -const BUFFER_CURSOR_POOL_SIZE = 10; -const BufferCursorPool = new class { - private _pool: BufferCursor[]; - private _len: number; - - constructor() { - this._pool = []; - for (let i = 0; i < BUFFER_CURSOR_POOL_SIZE; i++) { - this._pool[i] = new BufferCursor(0, 0, 0, 0); - } - this._len = this._pool.length; - } - - public put(cursor: BufferCursor): void { - if (this._len > this._pool.length) { - // oh, well - return; - } - this._pool[this._len++] = cursor; - } - - public take(): BufferCursor { - if (this._len === 0) { - // oh, well - console.log(`insufficient BufferCursor pool`); - return new BufferCursor(0, 0, 0, 0); - } - const result = this._pool[this._len - 1]; - this._pool[this._len--] = null; - return result; - } -}; - -class BufferSnapshot implements ITextSnapshot { - - private readonly _pieces: BufferPiece[]; - private readonly _piecesLength: number; - private readonly _BOM: string; - private _piecesIndex: number; - - constructor(pieces: BufferPiece[], BOM: string) { - this._pieces = pieces; - this._piecesLength = this._pieces.length; - this._BOM = BOM; - this._piecesIndex = 0; - } - - public read(): string { - if (this._piecesIndex >= this._piecesLength) { - return null; - } - - let result: string = null; - if (this._piecesIndex === 0) { - result = this._BOM + this._pieces[this._piecesIndex].text; - } else { - result = this._pieces[this._piecesIndex].text; - } - - this._piecesIndex++; - return result; - } -} - -class Buffer { - - private _minLeafLength: number; - private _maxLeafLength: number; - private _idealLeafLength: number; - - private _eol: '\r\n' | '\n'; - private _eolLength: number; - - private _leafs: BufferPiece[]; - private _nodes: BufferNodes; - private _nodesCount: number; - private _leafsStart: number; - private _leafsEnd: number; - - constructor(pieces: BufferPiece[], minLeafLength: number, maxLeafLength: number, eol: '\r\n' | '\n') { - if (!(2 * minLeafLength >= maxLeafLength)) { - throw new Error(`assertion violation`); - } - - this._minLeafLength = minLeafLength; - this._maxLeafLength = maxLeafLength; - this._idealLeafLength = (minLeafLength + maxLeafLength) >>> 1; - - this._eol = eol; - this._eolLength = this._eol.length; - - this._leafs = pieces; - this._nodes = null; - this._nodesCount = 0; - this._leafsStart = 0; - this._leafsEnd = 0; - - this._rebuildNodes(); - } - - equals(other: Buffer): boolean { - return Buffer.equals(this, other); - } - - private static equals(a: Buffer, b: Buffer): boolean { - const aLength = a.getLength(); - const bLength = b.getLength(); - if (aLength !== bLength) { - return false; - } - if (a.getLineCount() !== b.getLineCount()) { - return false; - } - - let remaining = aLength; - let aLeafIndex = -1, aLeaf = null, aLeafLength = 0, aLeafRemaining = 0; - let bLeafIndex = -1, bLeaf = null, bLeafLength = 0, bLeafRemaining = 0; - - while (remaining > 0) { - if (aLeafRemaining === 0) { - aLeafIndex++; - aLeaf = a._leafs[aLeafIndex]; - aLeafLength = aLeaf.length(); - aLeafRemaining = aLeafLength; - } - - if (bLeafRemaining === 0) { - bLeafIndex++; - bLeaf = b._leafs[bLeafIndex]; - bLeafLength = bLeaf.length(); - bLeafRemaining = bLeafLength; - } - - let consuming = Math.min(aLeafRemaining, bLeafRemaining); - - let aStr = aLeaf.substr(aLeafLength - aLeafRemaining, consuming); - let bStr = bLeaf.substr(bLeafLength - bLeafRemaining, consuming); - - if (aStr !== bStr) { - return false; - } - - remaining -= consuming; - aLeafRemaining -= consuming; - bLeafRemaining -= consuming; - } - - return true; - } - - public getEOL(): string { - return this._eol; - } - - private _rebuildNodes() { - const leafsCount = this._leafs.length; - - this._nodesCount = (1 << log2(leafsCount)); - this._leafsStart = this._nodesCount; - this._leafsEnd = this._leafsStart + leafsCount; - - this._nodes = new BufferNodes(this._nodesCount); - for (let i = this._nodesCount - 1; i >= 1; i--) { - this._updateSingleNode(i); - } - } - - private _updateSingleNode(nodeIndex: number): void { - const left = LEFT_CHILD(nodeIndex); - const right = RIGHT_CHILD(nodeIndex); - - let length = 0; - let newLineCount = 0; - - if (this.IS_NODE(left)) { - length += this._nodes.length[left]; - newLineCount += this._nodes.newLineCount[left]; - } else if (this.IS_LEAF(left)) { - const leaf = this._leafs[this.NODE_TO_LEAF_INDEX(left)]; - length += leaf.length(); - newLineCount += leaf.newLineCount(); - } - - if (this.IS_NODE(right)) { - length += this._nodes.length[right]; - newLineCount += this._nodes.newLineCount[right]; - } else if (this.IS_LEAF(right)) { - const leaf = this._leafs[this.NODE_TO_LEAF_INDEX(right)]; - length += leaf.length(); - newLineCount += leaf.newLineCount(); - } - - this._nodes.length[nodeIndex] = length; - this._nodes.newLineCount[nodeIndex] = newLineCount; - } - - private _findOffset(offset: number, result: BufferCursor): boolean { - if (offset > this._nodes.length[1]) { - return false; - } - - let it = 1; - let searchOffset = offset; - let leafStartOffset = 0; - let leafStartNewLineCount = 0; - while (!this.IS_LEAF(it)) { - const left = LEFT_CHILD(it); - const right = RIGHT_CHILD(it); - - let leftNewLineCount = 0; - let leftLength = 0; - if (this.IS_NODE(left)) { - leftNewLineCount = this._nodes.newLineCount[left]; - leftLength = this._nodes.length[left]; - } else if (this.IS_LEAF(left)) { - const leaf = this._leafs[this.NODE_TO_LEAF_INDEX(left)]; - leftNewLineCount = leaf.newLineCount(); - leftLength = leaf.length(); - } - - let rightLength = 0; - if (this.IS_NODE(right)) { - rightLength += this._nodes.length[right]; - } else if (this.IS_LEAF(right)) { - rightLength += this._leafs[this.NODE_TO_LEAF_INDEX(right)].length(); - } - - if (searchOffset < leftLength || rightLength === 0) { - // go left - it = left; - } else { - // go right - searchOffset -= leftLength; - leafStartOffset += leftLength; - leafStartNewLineCount += leftNewLineCount; - it = right; - } - } - it = this.NODE_TO_LEAF_INDEX(it); - - result.set(offset, it, leafStartOffset, leafStartNewLineCount); - return true; - } - - private _findOffsetCloseAfter(offset: number, start: BufferCursor, result: BufferCursor): boolean { - if (offset > this._nodes.length[1]) { - return false; - } - - let innerOffset = offset - start.leafStartOffset; - const leafsCount = this._leafs.length; - - let leafIndex = start.leafIndex; - let leafStartOffset = start.leafStartOffset; - let leafStartNewLineCount = start.leafStartNewLineCount; - - while (true) { - const leaf = this._leafs[leafIndex]; - - if (innerOffset < leaf.length() || (innerOffset === leaf.length() && leafIndex + 1 === leafsCount)) { - result.set(offset, leafIndex, leafStartOffset, leafStartNewLineCount); - return true; - } - - leafIndex++; - - if (leafIndex >= leafsCount) { - result.set(offset, leafIndex, leafStartOffset, leafStartNewLineCount); - return true; - } - - leafStartOffset += leaf.length(); - leafStartNewLineCount += leaf.newLineCount(); - innerOffset -= leaf.length(); - } - } - - private _findLineStart(lineNumber: number, result: BufferCursor): boolean { - let lineIndex = lineNumber - 1; - if (lineIndex < 0 || lineIndex > this._nodes.newLineCount[1]) { - result.set(0, 0, 0, 0); - return false; - } - - let it = 1; - let leafStartOffset = 0; - let leafStartNewLineCount = 0; - while (!this.IS_LEAF(it)) { - const left = LEFT_CHILD(it); - const right = RIGHT_CHILD(it); - - let leftNewLineCount = 0; - let leftLength = 0; - if (this.IS_NODE(left)) { - leftNewLineCount = this._nodes.newLineCount[left]; - leftLength = this._nodes.length[left]; - } else if (this.IS_LEAF(left)) { - const leaf = this._leafs[this.NODE_TO_LEAF_INDEX(left)]; - leftNewLineCount = leaf.newLineCount(); - leftLength = leaf.length(); - } - - if (lineIndex <= leftNewLineCount) { - // go left - it = left; - continue; - } - - // go right - lineIndex -= leftNewLineCount; - leafStartOffset += leftLength; - leafStartNewLineCount += leftNewLineCount; - it = right; - } - it = this.NODE_TO_LEAF_INDEX(it); - - const innerLineStartOffset = (lineIndex === 0 ? 0 : this._leafs[it].lineStartFor(lineIndex - 1)); - - result.set(leafStartOffset + innerLineStartOffset, it, leafStartOffset, leafStartNewLineCount); - return true; - } - - private _findLineEnd(start: BufferCursor, lineNumber: number, result: BufferCursor): void { - let innerLineIndex = lineNumber - 1 - start.leafStartNewLineCount; - const leafsCount = this._leafs.length; - - let leafIndex = start.leafIndex; - let leafStartOffset = start.leafStartOffset; - let leafStartNewLineCount = start.leafStartNewLineCount; - while (true) { - const leaf = this._leafs[leafIndex]; - - if (innerLineIndex < leaf.newLineCount()) { - const lineEndOffset = this._leafs[leafIndex].lineStartFor(innerLineIndex); - result.set(leafStartOffset + lineEndOffset, leafIndex, leafStartOffset, leafStartNewLineCount); - return; - } - - leafIndex++; - - if (leafIndex >= leafsCount) { - result.set(leafStartOffset + leaf.length(), leafIndex - 1, leafStartOffset, leafStartNewLineCount); - return; - } - - leafStartOffset += leaf.length(); - leafStartNewLineCount += leaf.newLineCount(); - innerLineIndex = 0; - } - } - - private _findLine(lineNumber: number, start: BufferCursor, end: BufferCursor): boolean { - if (!this._findLineStart(lineNumber, start)) { - return false; - } - - this._findLineEnd(start, lineNumber, end); - return true; - } - - public getLength(): number { - return this._nodes.length[1]; - } - - public getLineCount(): number { - return this._nodes.newLineCount[1] + 1; - } - - public getLineContent(lineNumber: number): string { - const start = BufferCursorPool.take(); - const end = BufferCursorPool.take(); - - if (!this._findLine(lineNumber, start, end)) { - BufferCursorPool.put(start); - BufferCursorPool.put(end); - throw new Error(`Line not found`); - } - - let result: string; - if (lineNumber === this.getLineCount()) { - // last line is not trailed by an eol - result = this.extractString(start, end.offset - start.offset); - } else { - result = this.extractString(start, end.offset - start.offset - this._eolLength); - } - - BufferCursorPool.put(start); - BufferCursorPool.put(end); - return result; - } - - public getLineCharCode(lineNumber: number, index: number): number { - const start = BufferCursorPool.take(); - - if (!this._findLineStart(lineNumber, start)) { - BufferCursorPool.put(start); - throw new Error(`Line not found`); - } - - const tmp = BufferCursorPool.take(); - this._findOffsetCloseAfter(start.offset + index, start, tmp); - const result = this._leafs[tmp.leafIndex].charCodeAt(tmp.offset - tmp.leafStartNewLineCount); - BufferCursorPool.put(tmp); - - BufferCursorPool.put(start); - return result; - } - - public getLineLength(lineNumber: number): number { - const start = BufferCursorPool.take(); - const end = BufferCursorPool.take(); - - if (!this._findLine(lineNumber, start, end)) { - BufferCursorPool.put(start); - BufferCursorPool.put(end); - throw new Error(`Line not found`); - } - - let result: number; - if (lineNumber === this.getLineCount()) { - // last line is not trailed by an eol - result = end.offset - start.offset; - } else { - result = end.offset - start.offset - this._eolLength; - } - - BufferCursorPool.put(start); - BufferCursorPool.put(end); - return result; - } - - public getLineFirstNonWhitespaceIndex(lineNumber: number): number { - const start = BufferCursorPool.take(); - - if (!this._findLineStart(lineNumber, start)) { - BufferCursorPool.put(start); - throw new Error(`Line not found`); - } - - let leafIndex = start.leafIndex; - let searchStartOffset = start.offset - start.leafStartOffset; - BufferCursorPool.put(start); - - const leafsCount = this._leafs.length; - let totalDelta = 0; - while (true) { - const leaf = this._leafs[leafIndex]; - - const leafResult = leaf.findLineFirstNonWhitespaceIndex(searchStartOffset); - if (leafResult === -2) { - // reached EOL - return -1; - } - if (leafResult !== -1) { - return (leafResult - searchStartOffset) + totalDelta; - } - - leafIndex++; - - if (leafIndex >= leafsCount) { - return -1; - } - - totalDelta += (leaf.length() - searchStartOffset); - searchStartOffset = 0; - } - } - - public getLineLastNonWhitespaceIndex(lineNumber: number): number { - const start = BufferCursorPool.take(); - const end = BufferCursorPool.take(); - - if (!this._findLineStart(lineNumber, start)) { - BufferCursorPool.put(start); - BufferCursorPool.put(end); - throw new Error(`Line not found`); - } - - this._findLineEnd(start, lineNumber, end); - - const startOffset = start.offset; - const endOffset = end.offset; - let leafIndex = end.leafIndex; - let searchStartOffset = end.offset - end.leafStartOffset - this._eolLength; - - BufferCursorPool.put(start); - BufferCursorPool.put(end); - - let totalDelta = 0; - while (true) { - const leaf = this._leafs[leafIndex]; - - const leafResult = leaf.findLineLastNonWhitespaceIndex(searchStartOffset); - if (leafResult === -2) { - // reached EOL - return -1; - } - if (leafResult !== -1) { - const delta = (searchStartOffset - 1 - leafResult); - const absoluteOffset = (endOffset - this._eolLength) - delta - totalDelta; - return absoluteOffset - startOffset; - } - - leafIndex--; - - if (leafIndex < 0) { - return -1; - } - - totalDelta += searchStartOffset; - searchStartOffset = leaf.length(); - } - } - - public getLinesContent(): string[] { - let result: string[] = new Array(this.getLineCount()); - let resultIndex = 0; - - let currentLine = ''; - for (let leafIndex = 0, leafsCount = this._leafs.length; leafIndex < leafsCount; leafIndex++) { - const leaf = this._leafs[leafIndex]; - const leafNewLineCount = leaf.newLineCount(); - - if (leafNewLineCount === 0) { - // special case => push entire leaf text - currentLine += leaf.text; - continue; - } - - let leafSubstrOffset = 0; - for (let newLineIndex = 0; newLineIndex < leafNewLineCount; newLineIndex++) { - const newLineStart = leaf.lineStartFor(newLineIndex); - currentLine += leaf.substr(leafSubstrOffset, newLineStart - leafSubstrOffset - this._eolLength); - result[resultIndex++] = currentLine; - - currentLine = ''; - leafSubstrOffset = newLineStart; - } - currentLine += leaf.substr(leafSubstrOffset, leaf.length()); - } - result[resultIndex++] = currentLine; - - return result; - } - - public extractString(start: BufferCursor, len: number): string { - if (!(start.offset + len <= this._nodes.length[1])) { - throw new Error(`assertion violation`); - } - - let innerLeafOffset = start.offset - start.leafStartOffset; - let leafIndex = start.leafIndex; - let res = ''; - while (len > 0) { - const leaf = this._leafs[leafIndex]; - const cnt = Math.min(len, leaf.length() - innerLeafOffset); - res += leaf.substr(innerLeafOffset, cnt); - - len -= cnt; - innerLeafOffset = 0; - - if (len === 0) { - break; - } - - leafIndex++; - } - return res; - } - - private _getOffsetAt(lineNumber: number, column: number, result: BufferCursor): boolean { - const lineStart = BufferCursorPool.take(); - - if (!this._findLineStart(lineNumber, lineStart)) { - BufferCursorPool.put(lineStart); - return false; - } - - const startOffset = lineStart.offset + column - 1; - if (!this._findOffsetCloseAfter(startOffset, lineStart, result)) { - BufferCursorPool.put(lineStart); - return false; - } - - BufferCursorPool.put(lineStart); - return true; - } - - public convertPositionToOffset(lineNumber: number, column: number): number { - const r = BufferCursorPool.take(); - - if (!this._findLineStart(lineNumber, r)) { - BufferCursorPool.put(r); - throw new Error(`Position not found`); - } - - const result = r.offset + column - 1; - - BufferCursorPool.put(r); - return result; - } - - /** - * returns `lineNumber` - */ - private _findLineStartBeforeOffsetInLeaf(offset: number, leafIndex: number, leafStartOffset: number, leafStartNewLineCount: number, result: BufferCursor): number { - const leaf = this._leafs[leafIndex]; - const lineStartIndex = leaf.findLineStartBeforeOffset(offset - leafStartOffset); - const lineStartOffset = leafStartOffset + leaf.lineStartFor(lineStartIndex); - - result.set(lineStartOffset, leafIndex, leafStartOffset, leafStartNewLineCount); - return leafStartNewLineCount + lineStartIndex + 2; - } - - /** - * returns `lineNumber`. - */ - private _findLineStartBeforeOffset(offset: number, location: BufferCursor, result: BufferCursor): number { - - let leafIndex = location.leafIndex; - let leafStartOffset = location.leafStartOffset; - let leafStartNewLineCount = location.leafStartNewLineCount; - while (true) { - const leaf = this._leafs[leafIndex]; - - if (leaf.newLineCount() >= 1 && leaf.lineStartFor(0) + leafStartOffset <= offset) { - // must be in this leaf - return this._findLineStartBeforeOffsetInLeaf(offset, leafIndex, leafStartOffset, leafStartNewLineCount, result); - } - - // continue looking in previous leaf - leafIndex--; - - if (leafIndex < 0) { - result.set(0, 0, 0, 0); - return 1; - } - - leafStartOffset -= this._leafs[leafIndex].length(); - leafStartNewLineCount -= this._leafs[leafIndex].newLineCount(); - } - } - - public convertOffsetToPosition(offset: number): Position { - const r = BufferCursorPool.take(); - const lineStart = BufferCursorPool.take(); - - if (!this._findOffset(offset, r)) { - BufferCursorPool.put(r); - BufferCursorPool.put(lineStart); - throw new Error(`Offset not found`); - } - - const lineNumber = this._findLineStartBeforeOffset(offset, r, lineStart); - const column = offset - lineStart.offset + 1; - - BufferCursorPool.put(r); - BufferCursorPool.put(lineStart); - - return new Position(lineNumber, column); - } - - public convertOffsetLenToRange(offset: number, len: number): Range { - const r = BufferCursorPool.take(); - const lineStart = BufferCursorPool.take(); - - if (!this._findOffset(offset, r)) { - BufferCursorPool.put(r); - BufferCursorPool.put(lineStart); - throw new Error(`Offset not found`); - } - const startLineNumber = this._findLineStartBeforeOffset(offset, r, lineStart); - const startColumn = offset - lineStart.offset + 1; - - if (!this._findOffset(offset + len, r)) { - BufferCursorPool.put(r); - BufferCursorPool.put(lineStart); - throw new Error(`Offset not found`); - } - const endLineNumber = this._findLineStartBeforeOffset(offset + len, r, lineStart); - const endColumn = offset + len - lineStart.offset + 1; - - BufferCursorPool.put(r); - BufferCursorPool.put(lineStart); - - return new Range(startLineNumber, startColumn, endLineNumber, endColumn); - } - - public getValueInRange(range: Range): string { - const start = BufferCursorPool.take(); - - if (!this._getOffsetAt(range.startLineNumber, range.startColumn, start)) { - BufferCursorPool.put(start); - throw new Error(`Line not found`); - } - - const endOffset = this.convertPositionToOffset(range.endLineNumber, range.endColumn); - const result = this.extractString(start, endOffset - start.offset); - - BufferCursorPool.put(start); - return result; - } - - public createSnapshot(BOM: string): ITextSnapshot { - return new BufferSnapshot(this._leafs, BOM); - } - - public getValueLengthInRange(range: Range): number { - const startOffset = this.convertPositionToOffset(range.startLineNumber, range.startColumn); - const endOffset = this.convertPositionToOffset(range.endLineNumber, range.endColumn); - return endOffset - startOffset; - } - - //#region Editing - - private _mergeAdjacentEdits(edits: OffsetLenEdit[]): OffsetLenEdit[] { - // Check if we must merge adjacent edits - let merged: OffsetLenEdit[] = [], mergedLength = 0; - let prev = edits[0]; - for (let i = 1, len = edits.length; i < len; i++) { - const curr = edits[i]; - if (prev.offset + prev.length === curr.offset) { - // merge into `prev` - prev.length = prev.length + curr.length; - prev.text = prev.text + curr.text; - } else { - merged[mergedLength++] = prev; - prev = curr; - } - } - merged[mergedLength++] = prev; - - return merged; - } - - private _resolveEdits(edits: OffsetLenEdit[]): InternalOffsetLenEdit[] { - edits = this._mergeAdjacentEdits(edits); - - let result: InternalOffsetLenEdit[] = []; - let tmp = new BufferCursor(0, 0, 0, 0); - let tmp2 = new BufferCursor(0, 0, 0, 0); - for (let i = 0, len = edits.length; i < len; i++) { - const edit = edits[i]; - - let text = edit.text; - - this._findOffset(edit.offset, tmp); - let startLeafIndex = tmp.leafIndex; - let startInnerOffset = tmp.offset - tmp.leafStartOffset; - if (startInnerOffset > 0) { - const startLeaf = this._leafs[startLeafIndex]; - const charBefore = startLeaf.charCodeAt(startInnerOffset - 1); - if (charBefore === CharCode.CarriageReturn) { - // include the replacement of \r in the edit - text = '\r' + text; - - this._findOffsetCloseAfter(edit.offset - 1, tmp, tmp2); - startLeafIndex = tmp2.leafIndex; - startInnerOffset = tmp2.offset - tmp2.leafStartOffset; - // this._findOffset(edit.offset - 1, tmp); - // startLeafIndex = tmp.leafIndex; - // startInnerOffset = tmp.offset - tmp.leafStartOffset; - } - } - - this._findOffset(edit.offset + edit.length, tmp); - let endLeafIndex = tmp.leafIndex; - let endInnerOffset = tmp.offset - tmp.leafStartOffset; - const endLeaf = this._leafs[endLeafIndex]; - if (endInnerOffset < endLeaf.length()) { - const charAfter = endLeaf.charCodeAt(endInnerOffset); - if (charAfter === CharCode.LineFeed) { - // include the replacement of \n in the edit - text = text + '\n'; - - this._findOffsetCloseAfter(edit.offset + edit.length + 1, tmp, tmp2); - endLeafIndex = tmp2.leafIndex; - endInnerOffset = tmp2.offset - tmp2.leafStartOffset; - // this._findOffset(edit.offset + edit.length + 1, tmp); - // endLeafIndex = tmp.leafIndex; - // endInnerOffset = tmp.offset - tmp.leafStartOffset; - } - } - - result[i] = new InternalOffsetLenEdit( - startLeafIndex, startInnerOffset, - endLeafIndex, endInnerOffset, - text - ); - } - - return result; - } - - private _pushLeafReplacement(startLeafIndex: number, endLeafIndex: number, replacements: LeafReplacement[]): LeafReplacement { - const res = new LeafReplacement(startLeafIndex, endLeafIndex, []); - replacements.push(res); - return res; - } - - private _flushLeafEdits(accumulatedLeafIndex: number, accumulatedLeafEdits: LeafOffsetLenEdit[], replacements: LeafReplacement[]): void { - if (accumulatedLeafEdits.length > 0) { - const rep = this._pushLeafReplacement(accumulatedLeafIndex, accumulatedLeafIndex, replacements); - BufferPiece.replaceOffsetLen(this._leafs[accumulatedLeafIndex], accumulatedLeafEdits, this._idealLeafLength, this._maxLeafLength, rep.replacements); - } - accumulatedLeafEdits.length = 0; - } - - private _pushLeafEdits(start: number, length: number, text: string, accumulatedLeafEdits: LeafOffsetLenEdit[]): void { - if (length !== 0 || text.length !== 0) { - accumulatedLeafEdits.push(new LeafOffsetLenEdit(start, length, text)); - } - } - - private _appendLeaf(leaf: BufferPiece, leafs: BufferPiece[], prevLeaf: BufferPiece): BufferPiece { - if (prevLeaf === null) { - leafs.push(leaf); - prevLeaf = leaf; - return prevLeaf; - } - - let prevLeafLength = prevLeaf.length(); - let currLeafLength = leaf.length(); - - if ((prevLeafLength < this._minLeafLength || currLeafLength < this._minLeafLength) && prevLeafLength + currLeafLength <= this._maxLeafLength) { - const joinedLeaf = BufferPiece.join(prevLeaf, leaf); - leafs[leafs.length - 1] = joinedLeaf; - prevLeaf = joinedLeaf; - return prevLeaf; - } - - const lastChar = prevLeaf.charCodeAt(prevLeafLength - 1); - const firstChar = leaf.charCodeAt(0); - - if ( - (lastChar >= 0xd800 && lastChar <= 0xdbff) || (lastChar === CharCode.CarriageReturn && firstChar === CharCode.LineFeed) - ) { - const modifiedPrevLeaf = BufferPiece.deleteLastChar(prevLeaf); - leafs[leafs.length - 1] = modifiedPrevLeaf; - - const modifiedLeaf = BufferPiece.insertFirstChar(leaf, lastChar); - leaf = modifiedLeaf; - } - - leafs.push(leaf); - prevLeaf = leaf; - return prevLeaf; - } - - private static _compareEdits(a: OffsetLenEdit, b: OffsetLenEdit): number { - if (a.offset === b.offset) { - if (a.length === b.length) { - return (a.initialIndex - b.initialIndex); - } - return (a.length - b.length); - } - return a.offset - b.offset; - } - - public replaceOffsetLen(_edits: OffsetLenEdit[]): void { - _edits.sort(Buffer._compareEdits); - - const initialLeafLength = this._leafs.length; - const edits = this._resolveEdits(_edits); - - let accumulatedLeafIndex = 0; - let accumulatedLeafEdits: LeafOffsetLenEdit[] = []; - let replacements: LeafReplacement[] = []; - - for (let i = 0, len = edits.length; i < len; i++) { - const edit = edits[i]; - - const startLeafIndex = edit.startLeafIndex; - const endLeafIndex = edit.endLeafIndex; - - if (startLeafIndex !== accumulatedLeafIndex) { - this._flushLeafEdits(accumulatedLeafIndex, accumulatedLeafEdits, replacements); - accumulatedLeafIndex = startLeafIndex; - } - - const leafEditStart = edit.startInnerOffset; - const leafEditEnd = (startLeafIndex === endLeafIndex ? edit.endInnerOffset : this._leafs[startLeafIndex].length()); - this._pushLeafEdits(leafEditStart, leafEditEnd - leafEditStart, edit.text, accumulatedLeafEdits); - - if (startLeafIndex < endLeafIndex) { - this._flushLeafEdits(accumulatedLeafIndex, accumulatedLeafEdits, replacements); - accumulatedLeafIndex = endLeafIndex; - - // delete leafs in the middle - if (startLeafIndex + 1 < endLeafIndex) { - this._pushLeafReplacement(startLeafIndex + 1, endLeafIndex - 1, replacements); - } - - // delete on last leaf - const leafEditStart = 0; - const leafEditEnd = edit.endInnerOffset; - this._pushLeafEdits(leafEditStart, leafEditEnd - leafEditStart, '', accumulatedLeafEdits); - } - } - this._flushLeafEdits(accumulatedLeafIndex, accumulatedLeafEdits, replacements); - - let leafs: BufferPiece[] = []; - let leafIndex = 0; - let prevLeaf: BufferPiece = null; - - for (let i = 0, len = replacements.length; i < len; i++) { - const replaceStartLeafIndex = replacements[i].startLeafIndex; - const replaceEndLeafIndex = replacements[i].endLeafIndex; - const innerLeafs = replacements[i].replacements; - - // add leafs to the left of this replace op. - while (leafIndex < replaceStartLeafIndex) { - prevLeaf = this._appendLeaf(this._leafs[leafIndex], leafs, prevLeaf); - leafIndex++; - } - - // delete leafs that get replaced. - while (leafIndex <= replaceEndLeafIndex) { - leafIndex++; - } - - // add new leafs. - for (let j = 0, lenJ = innerLeafs.length; j < lenJ; j++) { - prevLeaf = this._appendLeaf(innerLeafs[j], leafs, prevLeaf); - } - } - - // add remaining leafs to the right of the last replacement. - while (leafIndex < initialLeafLength) { - prevLeaf = this._appendLeaf(this._leafs[leafIndex], leafs, prevLeaf); - leafIndex++; - } - - if (leafs.length === 0) { - // don't leave behind an empty leafs array - leafs.push(new BufferPiece('')); - } - - this._leafs = leafs; - this._rebuildNodes(); - } - - public setEOL(newEOL: '\r\n' | '\n'): void { - let leafs: BufferPiece[] = []; - for (let i = 0, len = this._leafs.length; i < len; i++) { - leafs[i] = BufferPiece.normalizeEOL(this._leafs[i], newEOL); - } - this._leafs = leafs; - this._rebuildNodes(); - this._eol = newEOL; - this._eolLength = this._eol.length; - } - - //#endregion - - private IS_NODE(i: number): boolean { - return (i < this._nodesCount); - } - private IS_LEAF(i: number): boolean { - return (i >= this._leafsStart && i < this._leafsEnd); - } - private NODE_TO_LEAF_INDEX(i: number): number { - return (i - this._leafsStart); - } - // private LEAF_TO_NODE_INDEX(i: number): number { - // return (i + this._leafsStart); - // } -} - -function log2(n: number): number { - let v = 1; - for (let pow = 1; ; pow++) { - v = v << 1; - if (v >= n) { - return pow; - } - } - // return -1; -} - -function LEFT_CHILD(i: number): number { - return (i << 1); -} - -function RIGHT_CHILD(i: number): number { - return (i << 1) + 1; -} diff --git a/src/vs/editor/common/model/chunksTextBuffer/chunksTextBufferBuilder.ts b/src/vs/editor/common/model/chunksTextBuffer/chunksTextBufferBuilder.ts deleted file mode 100644 index 12eaf167b28..00000000000 --- a/src/vs/editor/common/model/chunksTextBuffer/chunksTextBufferBuilder.ts +++ /dev/null @@ -1,186 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -'use strict'; - -import * as strings from 'vs/base/common/strings'; -import { ITextBufferBuilder, ITextBufferFactory, ITextBuffer, DefaultEndOfLine } from 'vs/editor/common/model'; -import { BufferPiece, createLineStarts } from 'vs/editor/common/model/chunksTextBuffer/bufferPiece'; -import { ChunksTextBuffer } from 'vs/editor/common/model/chunksTextBuffer/chunksTextBuffer'; -import { CharCode } from 'vs/base/common/charCode'; - -export class TextBufferFactory implements ITextBufferFactory { - - constructor( - private readonly _pieces: BufferPiece[], - private readonly _averageChunkSize: number, - private readonly _BOM: string, - private readonly _cr: number, - private readonly _lf: number, - private readonly _crlf: number, - private readonly _containsRTL: boolean, - private readonly _isBasicASCII: boolean, - ) { - } - - /** - * if text source is empty or with precisely one line, returns null. No end of line is detected. - * if text source contains more lines ending with '\r\n', returns '\r\n'. - * Otherwise returns '\n'. More lines end with '\n'. - */ - private _getEOL(defaultEOL: DefaultEndOfLine): '\r\n' | '\n' { - const totalEOLCount = this._cr + this._lf + this._crlf; - const totalCRCount = this._cr + this._crlf; - if (totalEOLCount === 0) { - // This is an empty file or a file with precisely one line - return (defaultEOL === DefaultEndOfLine.LF ? '\n' : '\r\n'); - } - if (totalCRCount > totalEOLCount / 2) { - // More than half of the file contains \r\n ending lines - return '\r\n'; - } - // At least one line more ends in \n - return '\n'; - } - - public create(defaultEOL: DefaultEndOfLine): ITextBuffer { - const eol = this._getEOL(defaultEOL); - let pieces = this._pieces; - - if ( - (eol === '\r\n' && (this._cr > 0 || this._lf > 0)) - || (eol === '\n' && (this._cr > 0 || this._crlf > 0)) - ) { - // Normalize pieces - for (let i = 0, len = pieces.length; i < len; i++) { - pieces[i] = BufferPiece.normalizeEOL(pieces[i], eol); - } - } - return new ChunksTextBuffer(pieces, this._averageChunkSize, this._BOM, eol, this._containsRTL, this._isBasicASCII); - } - - public getFirstLineText(lengthLimit: number): string { - const firstPiece = this._pieces[0]; - if (firstPiece.newLineCount() === 0) { - return firstPiece.substr(0, lengthLimit); - } - - const firstEOLOffset = firstPiece.lineStartFor(0); - return firstPiece.substr(0, Math.min(lengthLimit, firstEOLOffset)); - } -} - -export class ChunksTextBufferBuilder implements ITextBufferBuilder { - - private _rawPieces: BufferPiece[]; - private _hasPreviousChar: boolean; - private _previousChar: number; - private _averageChunkSize: number; - private _tmpLineStarts: number[]; - - private BOM: string; - private cr: number; - private lf: number; - private crlf: number; - private containsRTL: boolean; - private isBasicASCII: boolean; - - constructor() { - this._rawPieces = []; - this._hasPreviousChar = false; - this._previousChar = 0; - this._averageChunkSize = 0; - this._tmpLineStarts = []; - - this.BOM = ''; - this.cr = 0; - this.lf = 0; - this.crlf = 0; - this.containsRTL = false; - this.isBasicASCII = true; - } - - public acceptChunk(chunk: string): void { - if (chunk.length === 0) { - return; - } - - if (this._rawPieces.length === 0) { - if (strings.startsWithUTF8BOM(chunk)) { - this.BOM = strings.UTF8_BOM_CHARACTER; - chunk = chunk.substr(1); - } - } - - this._averageChunkSize = (this._averageChunkSize * this._rawPieces.length + chunk.length) / (this._rawPieces.length + 1); - - const lastChar = chunk.charCodeAt(chunk.length - 1); - if (lastChar === CharCode.CarriageReturn || (lastChar >= 0xd800 && lastChar <= 0xdbff)) { - // last character is \r or a high surrogate => keep it back - this._acceptChunk1(chunk.substr(0, chunk.length - 1), false); - this._hasPreviousChar = true; - this._previousChar = lastChar; - } else { - this._acceptChunk1(chunk, false); - this._hasPreviousChar = false; - this._previousChar = lastChar; - } - } - - private _acceptChunk1(chunk: string, allowEmptyStrings: boolean): void { - if (!allowEmptyStrings && chunk.length === 0) { - // Nothing to do - return; - } - - if (this._hasPreviousChar) { - this._acceptChunk2(chunk + String.fromCharCode(this._previousChar)); - } else { - this._acceptChunk2(chunk); - } - } - - private _acceptChunk2(chunk: string): void { - const lineStarts = createLineStarts(this._tmpLineStarts, chunk); - - this._rawPieces.push(new BufferPiece(chunk, lineStarts.lineStarts)); - this.cr += lineStarts.cr; - this.lf += lineStarts.lf; - this.crlf += lineStarts.crlf; - - if (this.isBasicASCII) { - this.isBasicASCII = lineStarts.isBasicASCII; - } - if (!this.isBasicASCII && !this.containsRTL) { - // No need to check if is basic ASCII - this.containsRTL = strings.containsRTL(chunk); - } - } - - public finish(): TextBufferFactory { - this._finish(); - return new TextBufferFactory(this._rawPieces, this._averageChunkSize, this.BOM, this.cr, this.lf, this.crlf, this.containsRTL, this.isBasicASCII); - } - - private _finish(): void { - if (this._rawPieces.length === 0) { - // no chunks => forcefully go through accept chunk - this._acceptChunk1('', true); - return; - } - - if (this._hasPreviousChar) { - this._hasPreviousChar = false; - - // recreate last chunk - const lastPiece = this._rawPieces[this._rawPieces.length - 1]; - const tmp = new BufferPiece(String.fromCharCode(this._previousChar)); - const newLastPiece = BufferPiece.join(lastPiece, tmp); - this._rawPieces[this._rawPieces.length - 1] = newLastPiece; - if (this._previousChar === CharCode.CarriageReturn) { - this.cr++; - } - } - } -} diff --git a/src/vs/editor/common/model/intervalTree.ts b/src/vs/editor/common/model/intervalTree.ts index 07f7200be6e..55636d2eace 100644 --- a/src/vs/editor/common/model/intervalTree.ts +++ b/src/vs/editor/common/model/intervalTree.ts @@ -12,14 +12,11 @@ import { IModelDecoration } from 'vs/editor/common/model'; // The red-black tree is based on the "Introduction to Algorithms" by Cormen, Leiserson and Rivest. // -/** - * The class name sort order must match the severity order. Highest severity last. - */ export const ClassName = { - EditorHintDecoration: 'squiggly-a-hint', - EditorInfoDecoration: 'squiggly-b-info', - EditorWarningDecoration: 'squiggly-c-warning', - EditorErrorDecoration: 'squiggly-d-error' + EditorHintDecoration: 'squiggly-hint', + EditorInfoDecoration: 'squiggly-info', + EditorWarningDecoration: 'squiggly-warning', + EditorErrorDecoration: 'squiggly-error' }; /** diff --git a/src/vs/editor/common/model/linesTextBuffer/linesTextBuffer.ts b/src/vs/editor/common/model/linesTextBuffer/linesTextBuffer.ts deleted file mode 100644 index 0d03ecd90a0..00000000000 --- a/src/vs/editor/common/model/linesTextBuffer/linesTextBuffer.ts +++ /dev/null @@ -1,660 +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 { Range } from 'vs/editor/common/core/range'; -import { Position } from 'vs/editor/common/core/position'; -import * as strings from 'vs/base/common/strings'; -import * as arrays from 'vs/base/common/arrays'; -import { PrefixSumComputer } from 'vs/editor/common/viewModel/prefixSumComputer'; -import { ISingleEditOperationIdentifier, IIdentifiedSingleEditOperation, EndOfLinePreference, ITextBuffer, ApplyEditsResult, IInternalModelContentChange } from 'vs/editor/common/model'; -import { ITextSnapshot } from 'vs/platform/files/common/files'; - -export interface IValidatedEditOperation { - sortIndex: number; - identifier: ISingleEditOperationIdentifier; - range: Range; - rangeOffset: number; - rangeLength: number; - lines: string[]; - forceMoveMarkers: boolean; - isAutoWhitespaceEdit: boolean; -} - -/** - * A processed string with its EOL resolved ready to be turned into an editor model. - */ -export interface ITextSource { - /** - * The text split into lines. - */ - readonly lines: string[]; - /** - * The BOM (leading character sequence of the file). - */ - readonly BOM: string; - /** - * The end of line sequence. - */ - readonly EOL: string; - /** - * The text contains Unicode characters classified as "R" or "AL". - */ - readonly containsRTL: boolean; - /** - * The text contains only characters inside the ASCII range 32-126 or \t \r \n - */ - readonly isBasicASCII: boolean; -} - -class LinesTextBufferSnapshot implements ITextSnapshot { - - private readonly _lines: string[]; - private readonly _linesLength: number; - private readonly _eol: string; - private readonly _bom: string; - private _lineIndex: number; - - constructor(lines: string[], eol: string, bom: string) { - this._lines = lines; - this._linesLength = this._lines.length; - this._eol = eol; - this._bom = bom; - this._lineIndex = 0; - } - - public read(): string { - if (this._lineIndex >= this._linesLength) { - return null; - } - - let result: string = null; - - if (this._lineIndex === 0) { - result = this._bom + this._lines[this._lineIndex]; - } else { - result = this._lines[this._lineIndex]; - } - - this._lineIndex++; - - if (this._lineIndex < this._linesLength) { - result += this._eol; - } - - return result; - } -} - -export class LinesTextBuffer implements ITextBuffer { - - private _lines: string[]; - private _BOM: string; - private _EOL: string; - private _mightContainRTL: boolean; - private _mightContainNonBasicASCII: boolean; - private _lineStarts: PrefixSumComputer; - - constructor(textSource: ITextSource) { - this._lines = textSource.lines.slice(0); - this._BOM = textSource.BOM; - this._EOL = textSource.EOL; - this._mightContainRTL = textSource.containsRTL; - this._mightContainNonBasicASCII = !textSource.isBasicASCII; - this._constructLineStarts(); - } - - private _constructLineStarts(): void { - const eolLength = this._EOL.length; - const linesLength = this._lines.length; - const lineStartValues = new Uint32Array(linesLength); - for (let i = 0; i < linesLength; i++) { - lineStartValues[i] = this._lines[i].length + eolLength; - } - this._lineStarts = new PrefixSumComputer(lineStartValues); - } - - public equals(other: ITextBuffer): boolean { - if (!(other instanceof LinesTextBuffer)) { - return false; - } - if (this._BOM !== other._BOM) { - return false; - } - if (this._EOL !== other._EOL) { - return false; - } - if (this._lines.length !== other._lines.length) { - return false; - } - for (let i = 0, len = this._lines.length; i < len; i++) { - if (this._lines[i] !== other._lines[i]) { - return false; - } - } - return true; - } - - public mightContainRTL(): boolean { - return this._mightContainRTL; - } - - public mightContainNonBasicASCII(): boolean { - return this._mightContainNonBasicASCII; - } - - public getBOM(): string { - return this._BOM; - } - - public getEOL(): string { - return this._EOL; - } - - public getOffsetAt(lineNumber: number, column: number): number { - return this._lineStarts.getAccumulatedValue(lineNumber - 2) + column - 1; - } - - public getPositionAt(offset: number): Position { - offset = Math.floor(offset); - offset = Math.max(0, offset); - - let out = this._lineStarts.getIndexOf(offset); - - let lineLength = this._lines[out.index].length; - - // Ensure we return a valid position - return new Position(out.index + 1, Math.min(out.remainder + 1, lineLength + 1)); - } - - public getRangeAt(offset: number, length: number): Range { - const startResult = this._lineStarts.getIndexOf(offset); - const startLineLength = this._lines[startResult.index].length; - const startColumn = Math.min(startResult.remainder + 1, startLineLength + 1); - - const endResult = this._lineStarts.getIndexOf(offset + length); - const endLineLength = this._lines[endResult.index].length; - const endColumn = Math.min(endResult.remainder + 1, endLineLength + 1); - - return new Range(startResult.index + 1, startColumn, endResult.index + 1, endColumn); - } - - private _getEndOfLine(eol: EndOfLinePreference): string { - switch (eol) { - case EndOfLinePreference.LF: - return '\n'; - case EndOfLinePreference.CRLF: - return '\r\n'; - case EndOfLinePreference.TextDefined: - return this.getEOL(); - } - throw new Error('Unknown EOL preference'); - } - - public getValueInRange(range: Range, eol: EndOfLinePreference): string { - if (range.isEmpty()) { - return ''; - } - - if (range.startLineNumber === range.endLineNumber) { - return this._lines[range.startLineNumber - 1].substring(range.startColumn - 1, range.endColumn - 1); - } - - const lineEnding = this._getEndOfLine(eol); - const startLineIndex = range.startLineNumber - 1; - const endLineIndex = range.endLineNumber - 1; - let resultLines: string[] = []; - - resultLines.push(this._lines[startLineIndex].substring(range.startColumn - 1)); - for (let i = startLineIndex + 1; i < endLineIndex; i++) { - resultLines.push(this._lines[i]); - } - resultLines.push(this._lines[endLineIndex].substring(0, range.endColumn - 1)); - - return resultLines.join(lineEnding); - } - - public createSnapshot(preserveBOM: boolean): ITextSnapshot { - return new LinesTextBufferSnapshot(this._lines.slice(0), this._EOL, preserveBOM ? this._BOM : ''); - } - - public getValueLengthInRange(range: Range, eol: EndOfLinePreference): number { - if (range.isEmpty()) { - return 0; - } - - if (range.startLineNumber === range.endLineNumber) { - return (range.endColumn - range.startColumn); - } - - let startOffset = this.getOffsetAt(range.startLineNumber, range.startColumn); - let endOffset = this.getOffsetAt(range.endLineNumber, range.endColumn); - return endOffset - startOffset; - } - - public getLineCount(): number { - return this._lines.length; - } - - public getLinesContent(): string[] { - return this._lines.slice(0); - } - - public getLength(): number { - return this._lineStarts.getTotalValue(); - } - - public getLineContent(lineNumber: number): string { - return this._lines[lineNumber - 1]; - } - - public getLineCharCode(lineNumber: number, index: number): number { - return this._lines[lineNumber - 1].charCodeAt(index); - } - - public getLineLength(lineNumber: number): number { - return this._lines[lineNumber - 1].length; - } - - public getLineFirstNonWhitespaceColumn(lineNumber: number): number { - const result = strings.firstNonWhitespaceIndex(this._lines[lineNumber - 1]); - if (result === -1) { - return 0; - } - return result + 1; - } - - public getLineLastNonWhitespaceColumn(lineNumber: number): number { - const result = strings.lastNonWhitespaceIndex(this._lines[lineNumber - 1]); - if (result === -1) { - return 0; - } - return result + 2; - } - - //#region Editing - - public setEOL(newEOL: '\r\n' | '\n'): void { - this._EOL = newEOL; - this._constructLineStarts(); - } - - private static _sortOpsAscending(a: IValidatedEditOperation, b: IValidatedEditOperation): number { - let r = Range.compareRangesUsingEnds(a.range, b.range); - if (r === 0) { - return a.sortIndex - b.sortIndex; - } - return r; - } - - private static _sortOpsDescending(a: IValidatedEditOperation, b: IValidatedEditOperation): number { - let r = Range.compareRangesUsingEnds(a.range, b.range); - if (r === 0) { - return b.sortIndex - a.sortIndex; - } - return -r; - } - - public applyEdits(rawOperations: IIdentifiedSingleEditOperation[], recordTrimAutoWhitespace: boolean): ApplyEditsResult { - if (rawOperations.length === 0) { - return new ApplyEditsResult([], [], []); - } - - let mightContainRTL = this._mightContainRTL; - let mightContainNonBasicASCII = this._mightContainNonBasicASCII; - let canReduceOperations = true; - - let operations: IValidatedEditOperation[] = []; - for (let i = 0; i < rawOperations.length; i++) { - let op = rawOperations[i]; - if (canReduceOperations && op._isTracked) { - canReduceOperations = false; - } - let validatedRange = op.range; - if (!mightContainRTL && op.text) { - // check if the new inserted text contains RTL - mightContainRTL = strings.containsRTL(op.text); - } - if (!mightContainNonBasicASCII && op.text) { - mightContainNonBasicASCII = !strings.isBasicASCII(op.text); - } - operations[i] = { - sortIndex: i, - identifier: op.identifier || null, - range: validatedRange, - rangeOffset: this.getOffsetAt(validatedRange.startLineNumber, validatedRange.startColumn), - rangeLength: this.getValueLengthInRange(validatedRange, EndOfLinePreference.TextDefined), - lines: op.text ? op.text.split(/\r\n|\r|\n/) : null, - forceMoveMarkers: op.forceMoveMarkers || false, - isAutoWhitespaceEdit: op.isAutoWhitespaceEdit || false - }; - } - - // Sort operations ascending - operations.sort(LinesTextBuffer._sortOpsAscending); - - for (let i = 0, count = operations.length - 1; i < count; i++) { - let rangeEnd = operations[i].range.getEndPosition(); - let nextRangeStart = operations[i + 1].range.getStartPosition(); - - if (nextRangeStart.isBefore(rangeEnd)) { - // overlapping ranges - throw new Error('Overlapping ranges are not allowed!'); - } - } - - if (canReduceOperations) { - operations = this._reduceOperations(operations); - } - - // Delta encode operations - let reverseRanges = LinesTextBuffer._getInverseEditRanges(operations); - let newTrimAutoWhitespaceCandidates: { lineNumber: number, oldContent: string }[] = []; - - for (let i = 0; i < operations.length; i++) { - let op = operations[i]; - let reverseRange = reverseRanges[i]; - - if (recordTrimAutoWhitespace && op.isAutoWhitespaceEdit && op.range.isEmpty()) { - // Record already the future line numbers that might be auto whitespace removal candidates on next edit - for (let lineNumber = reverseRange.startLineNumber; lineNumber <= reverseRange.endLineNumber; lineNumber++) { - let currentLineContent = ''; - if (lineNumber === reverseRange.startLineNumber) { - currentLineContent = this.getLineContent(op.range.startLineNumber); - if (strings.firstNonWhitespaceIndex(currentLineContent) !== -1) { - continue; - } - } - newTrimAutoWhitespaceCandidates.push({ lineNumber: lineNumber, oldContent: currentLineContent }); - } - } - } - - let reverseOperations: IIdentifiedSingleEditOperation[] = []; - for (let i = 0; i < operations.length; i++) { - let op = operations[i]; - let reverseRange = reverseRanges[i]; - - reverseOperations[i] = { - identifier: op.identifier, - range: reverseRange, - text: this.getValueInRange(op.range, EndOfLinePreference.TextDefined), - forceMoveMarkers: op.forceMoveMarkers - }; - } - - this._mightContainRTL = mightContainRTL; - this._mightContainNonBasicASCII = mightContainNonBasicASCII; - - const contentChanges = this._doApplyEdits(operations); - - let trimAutoWhitespaceLineNumbers: number[] = null; - if (recordTrimAutoWhitespace && newTrimAutoWhitespaceCandidates.length > 0) { - // sort line numbers auto whitespace removal candidates for next edit descending - newTrimAutoWhitespaceCandidates.sort((a, b) => b.lineNumber - a.lineNumber); - - trimAutoWhitespaceLineNumbers = []; - for (let i = 0, len = newTrimAutoWhitespaceCandidates.length; i < len; i++) { - let lineNumber = newTrimAutoWhitespaceCandidates[i].lineNumber; - if (i > 0 && newTrimAutoWhitespaceCandidates[i - 1].lineNumber === lineNumber) { - // Do not have the same line number twice - continue; - } - - let prevContent = newTrimAutoWhitespaceCandidates[i].oldContent; - let lineContent = this.getLineContent(lineNumber); - - if (lineContent.length === 0 || lineContent === prevContent || strings.firstNonWhitespaceIndex(lineContent) !== -1) { - continue; - } - - trimAutoWhitespaceLineNumbers.push(lineNumber); - } - } - - return new ApplyEditsResult( - reverseOperations, - contentChanges, - trimAutoWhitespaceLineNumbers - ); - } - - /** - * Transform operations such that they represent the same logic edit, - * but that they also do not cause OOM crashes. - */ - private _reduceOperations(operations: IValidatedEditOperation[]): IValidatedEditOperation[] { - if (operations.length < 1000) { - // We know from empirical testing that a thousand edits work fine regardless of their shape. - return operations; - } - - // At one point, due to how events are emitted and how each operation is handled, - // some operations can trigger a high ammount of temporary string allocations, - // that will immediately get edited again. - // e.g. a formatter inserting ridiculous ammounts of \n on a model with a single line - // Therefore, the strategy is to collapse all the operations into a huge single edit operation - return [this._toSingleEditOperation(operations)]; - } - - _toSingleEditOperation(operations: IValidatedEditOperation[]): IValidatedEditOperation { - let forceMoveMarkers = false, - firstEditRange = operations[0].range, - lastEditRange = operations[operations.length - 1].range, - entireEditRange = new Range(firstEditRange.startLineNumber, firstEditRange.startColumn, lastEditRange.endLineNumber, lastEditRange.endColumn), - lastEndLineNumber = firstEditRange.startLineNumber, - lastEndColumn = firstEditRange.startColumn, - result: string[] = []; - - for (let i = 0, len = operations.length; i < len; i++) { - let operation = operations[i], - range = operation.range; - - forceMoveMarkers = forceMoveMarkers || operation.forceMoveMarkers; - - // (1) -- Push old text - for (let lineNumber = lastEndLineNumber; lineNumber < range.startLineNumber; lineNumber++) { - if (lineNumber === lastEndLineNumber) { - result.push(this._lines[lineNumber - 1].substring(lastEndColumn - 1)); - } else { - result.push('\n'); - result.push(this._lines[lineNumber - 1]); - } - } - - if (range.startLineNumber === lastEndLineNumber) { - result.push(this._lines[range.startLineNumber - 1].substring(lastEndColumn - 1, range.startColumn - 1)); - } else { - result.push('\n'); - result.push(this._lines[range.startLineNumber - 1].substring(0, range.startColumn - 1)); - } - - // (2) -- Push new text - if (operation.lines) { - for (let j = 0, lenJ = operation.lines.length; j < lenJ; j++) { - if (j !== 0) { - result.push('\n'); - } - result.push(operation.lines[j]); - } - } - - lastEndLineNumber = operation.range.endLineNumber; - lastEndColumn = operation.range.endColumn; - } - - return { - sortIndex: 0, - identifier: operations[0].identifier, - range: entireEditRange, - rangeOffset: this.getOffsetAt(entireEditRange.startLineNumber, entireEditRange.startColumn), - rangeLength: this.getValueLengthInRange(entireEditRange, EndOfLinePreference.TextDefined), - lines: result.join('').split('\n'), - forceMoveMarkers: forceMoveMarkers, - isAutoWhitespaceEdit: false - }; - } - - private _setLineContent(lineNumber: number, content: string): void { - this._lines[lineNumber - 1] = content; - this._lineStarts.changeValue(lineNumber - 1, content.length + this._EOL.length); - } - - private _doApplyEdits(operations: IValidatedEditOperation[]): IInternalModelContentChange[] { - - // Sort operations descending - operations.sort(LinesTextBuffer._sortOpsDescending); - - let contentChanges: IInternalModelContentChange[] = []; - - for (let i = 0, len = operations.length; i < len; i++) { - const op = operations[i]; - - const startLineNumber = op.range.startLineNumber; - const startColumn = op.range.startColumn; - const endLineNumber = op.range.endLineNumber; - const endColumn = op.range.endColumn; - - if (startLineNumber === endLineNumber && startColumn === endColumn && (!op.lines || op.lines.length === 0)) { - // no-op - continue; - } - - const deletingLinesCnt = endLineNumber - startLineNumber; - const insertingLinesCnt = (op.lines ? op.lines.length - 1 : 0); - const editingLinesCnt = Math.min(deletingLinesCnt, insertingLinesCnt); - - for (let j = editingLinesCnt; j >= 0; j--) { - const editLineNumber = startLineNumber + j; - let editText = (op.lines ? op.lines[j] : ''); - - if (editLineNumber === startLineNumber || editLineNumber === endLineNumber) { - const editStartColumn = (editLineNumber === startLineNumber ? startColumn : 1); - const editEndColumn = (editLineNumber === endLineNumber ? endColumn : this.getLineLength(editLineNumber) + 1); - - editText = ( - this._lines[editLineNumber - 1].substring(0, editStartColumn - 1) - + editText - + this._lines[editLineNumber - 1].substring(editEndColumn - 1) - ); - } - - this._setLineContent(editLineNumber, editText); - } - - if (editingLinesCnt < deletingLinesCnt) { - // Must delete some lines - - const spliceStartLineNumber = startLineNumber + editingLinesCnt; - const endLineRemains = this._lines[endLineNumber - 1].substring(endColumn - 1); - - // Reconstruct first line - this._setLineContent(spliceStartLineNumber, this._lines[spliceStartLineNumber - 1] + endLineRemains); - - this._lines.splice(spliceStartLineNumber, endLineNumber - spliceStartLineNumber); - this._lineStarts.removeValues(spliceStartLineNumber, endLineNumber - spliceStartLineNumber); - } - - if (editingLinesCnt < insertingLinesCnt) { - // Must insert some lines - - const spliceLineNumber = startLineNumber + editingLinesCnt; - let spliceColumn = (spliceLineNumber === startLineNumber ? startColumn : 1); - if (op.lines) { - spliceColumn += op.lines[editingLinesCnt].length; - } - - // Split last line - const leftoverLine = this._lines[spliceLineNumber - 1].substring(spliceColumn - 1); - - this._setLineContent(spliceLineNumber, this._lines[spliceLineNumber - 1].substring(0, spliceColumn - 1)); - - // Lines in the middle - let newLines: string[] = new Array(insertingLinesCnt - editingLinesCnt); - let newLinesLengths = new Uint32Array(insertingLinesCnt - editingLinesCnt); - for (let j = editingLinesCnt + 1; j <= insertingLinesCnt; j++) { - newLines[j - editingLinesCnt - 1] = op.lines[j]; - newLinesLengths[j - editingLinesCnt - 1] = op.lines[j].length + this._EOL.length; - } - newLines[newLines.length - 1] += leftoverLine; - newLinesLengths[newLines.length - 1] += leftoverLine.length; - this._lines = arrays.arrayInsert(this._lines, startLineNumber + editingLinesCnt, newLines); - this._lineStarts.insertValues(startLineNumber + editingLinesCnt, newLinesLengths); - } - - const contentChangeRange = new Range(startLineNumber, startColumn, endLineNumber, endColumn); - const text = (op.lines ? op.lines.join(this.getEOL()) : ''); - contentChanges.push({ - range: contentChangeRange, - rangeLength: op.rangeLength, - text: text, - rangeOffset: op.rangeOffset, - forceMoveMarkers: op.forceMoveMarkers - }); - } - - return contentChanges; - } - - /** - * Assumes `operations` are validated and sorted ascending - */ - public static _getInverseEditRanges(operations: IValidatedEditOperation[]): Range[] { - let result: Range[] = []; - - let prevOpEndLineNumber: number; - let prevOpEndColumn: number; - let prevOp: IValidatedEditOperation = null; - for (let i = 0, len = operations.length; i < len; i++) { - let op = operations[i]; - - let startLineNumber: number; - let startColumn: number; - - if (prevOp) { - if (prevOp.range.endLineNumber === op.range.startLineNumber) { - startLineNumber = prevOpEndLineNumber; - startColumn = prevOpEndColumn + (op.range.startColumn - prevOp.range.endColumn); - } else { - startLineNumber = prevOpEndLineNumber + (op.range.startLineNumber - prevOp.range.endLineNumber); - startColumn = op.range.startColumn; - } - } else { - startLineNumber = op.range.startLineNumber; - startColumn = op.range.startColumn; - } - - let resultRange: Range; - - if (op.lines && op.lines.length > 0) { - // the operation inserts something - let lineCount = op.lines.length; - let firstLine = op.lines[0]; - let lastLine = op.lines[lineCount - 1]; - - if (lineCount === 1) { - // single line insert - resultRange = new Range(startLineNumber, startColumn, startLineNumber, startColumn + firstLine.length); - } else { - // multi line insert - resultRange = new Range(startLineNumber, startColumn, startLineNumber + lineCount - 1, lastLine.length + 1); - } - } else { - // There is nothing to insert - resultRange = new Range(startLineNumber, startColumn, startLineNumber, startColumn); - } - - prevOpEndLineNumber = resultRange.endLineNumber; - prevOpEndColumn = resultRange.endColumn; - - result.push(resultRange); - prevOp = op; - } - - return result; - } - - //#endregion -} diff --git a/src/vs/editor/common/model/linesTextBuffer/linesTextBufferBuilder.ts b/src/vs/editor/common/model/linesTextBuffer/linesTextBufferBuilder.ts deleted file mode 100644 index 3c46fd8d3e1..00000000000 --- a/src/vs/editor/common/model/linesTextBuffer/linesTextBufferBuilder.ts +++ /dev/null @@ -1,139 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -'use strict'; - -import * as strings from 'vs/base/common/strings'; -import { CharCode } from 'vs/base/common/charCode'; -import { ITextBufferBuilder, ITextBufferFactory, ITextBuffer, DefaultEndOfLine } from 'vs/editor/common/model'; -import { IRawTextSource, TextSource } from 'vs/editor/common/model/linesTextBuffer/textSource'; -import { LinesTextBuffer } from 'vs/editor/common/model/linesTextBuffer/linesTextBuffer'; - -export class TextBufferFactory implements ITextBufferFactory { - - constructor(public readonly rawTextSource: IRawTextSource) { - } - - public create(defaultEOL: DefaultEndOfLine): ITextBuffer { - const textSource = TextSource.fromRawTextSource(this.rawTextSource, defaultEOL); - return new LinesTextBuffer(textSource); - } - - public getFirstLineText(lengthLimit: number): string { - return this.rawTextSource.lines[0].substr(0, lengthLimit); - } -} - -class ModelLineBasedBuilder { - - private BOM: string; - private lines: string[]; - private currLineIndex: number; - - constructor() { - this.BOM = ''; - this.lines = []; - this.currLineIndex = 0; - } - - public acceptLines(lines: string[]): void { - if (this.currLineIndex === 0) { - // Remove the BOM (if present) - if (strings.startsWithUTF8BOM(lines[0])) { - this.BOM = strings.UTF8_BOM_CHARACTER; - lines[0] = lines[0].substr(1); - } - } - - for (let i = 0, len = lines.length; i < len; i++) { - this.lines[this.currLineIndex++] = lines[i]; - } - } - - public finish(carriageReturnCnt: number, containsRTL: boolean, isBasicASCII: boolean): TextBufferFactory { - return new TextBufferFactory({ - BOM: this.BOM, - lines: this.lines, - containsRTL: containsRTL, - totalCRCount: carriageReturnCnt, - isBasicASCII, - }); - } -} - -export class LinesTextBufferBuilder implements ITextBufferBuilder { - - private leftoverPrevChunk: string; - private leftoverEndsInCR: boolean; - private totalCRCount: number; - private lineBasedBuilder: ModelLineBasedBuilder; - private containsRTL: boolean; - private isBasicASCII: boolean; - - constructor() { - this.leftoverPrevChunk = ''; - this.leftoverEndsInCR = false; - this.totalCRCount = 0; - this.lineBasedBuilder = new ModelLineBasedBuilder(); - this.containsRTL = false; - this.isBasicASCII = true; - } - - private _updateCRCount(chunk: string): void { - // Count how many \r are present in chunk to determine the majority EOL sequence - let chunkCarriageReturnCnt = 0; - let lastCarriageReturnIndex = -1; - while ((lastCarriageReturnIndex = chunk.indexOf('\r', lastCarriageReturnIndex + 1)) !== -1) { - chunkCarriageReturnCnt++; - } - this.totalCRCount += chunkCarriageReturnCnt; - } - - public acceptChunk(chunk: string): void { - if (chunk.length === 0) { - return; - } - - this._updateCRCount(chunk); - - if (!this.containsRTL) { - this.containsRTL = strings.containsRTL(chunk); - } - if (this.isBasicASCII) { - this.isBasicASCII = strings.isBasicASCII(chunk); - } - - // Avoid dealing with a chunk that ends in \r (push the \r to the next chunk) - if (this.leftoverEndsInCR) { - chunk = '\r' + chunk; - } - if (chunk.charCodeAt(chunk.length - 1) === CharCode.CarriageReturn) { - this.leftoverEndsInCR = true; - chunk = chunk.substr(0, chunk.length - 1); - } else { - this.leftoverEndsInCR = false; - } - - let lines = chunk.split(/\r\n|\r|\n/); - - if (lines.length === 1) { - // no \r or \n encountered - this.leftoverPrevChunk += lines[0]; - return; - } - - lines[0] = this.leftoverPrevChunk + lines[0]; - this.lineBasedBuilder.acceptLines(lines.slice(0, lines.length - 1)); - this.leftoverPrevChunk = lines[lines.length - 1]; - } - - public finish(): TextBufferFactory { - let finalLines = [this.leftoverPrevChunk]; - if (this.leftoverEndsInCR) { - finalLines.push(''); - } - this.lineBasedBuilder.acceptLines(finalLines); - return this.lineBasedBuilder.finish(this.totalCRCount, this.containsRTL, this.isBasicASCII); - } -} diff --git a/src/vs/editor/common/model/linesTextBuffer/textSource.ts b/src/vs/editor/common/model/linesTextBuffer/textSource.ts deleted file mode 100644 index 79c06f1c9f1..00000000000 --- a/src/vs/editor/common/model/linesTextBuffer/textSource.ts +++ /dev/null @@ -1,66 +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 { DefaultEndOfLine } from 'vs/editor/common/model'; -import { ITextSource } from 'vs/editor/common/model/linesTextBuffer/linesTextBuffer'; - -/** - * A processed string ready to be turned into an editor model. - */ -export interface IRawTextSource { - /** - * The text split into lines. - */ - readonly lines: string[]; - /** - * The BOM (leading character sequence of the file). - */ - readonly BOM: string; - /** - * The number of lines ending with '\r\n' - */ - readonly totalCRCount: number; - /** - * The text contains Unicode characters classified as "R" or "AL". - */ - readonly containsRTL: boolean; - /** - * The text contains only characters inside the ASCII range 32-126 or \t \r \n - */ - readonly isBasicASCII: boolean; -} - -export class TextSource { - - /** - * if text source is empty or with precisely one line, returns null. No end of line is detected. - * if text source contains more lines ending with '\r\n', returns '\r\n'. - * Otherwise returns '\n'. More lines end with '\n'. - */ - private static _getEOL(rawTextSource: IRawTextSource, defaultEOL: DefaultEndOfLine): '\r\n' | '\n' { - const lineFeedCnt = rawTextSource.lines.length - 1; - if (lineFeedCnt === 0) { - // This is an empty file or a file with precisely one line - return (defaultEOL === DefaultEndOfLine.LF ? '\n' : '\r\n'); - } - if (rawTextSource.totalCRCount > lineFeedCnt / 2) { - // More than half of the file contains \r\n ending lines - return '\r\n'; - } - // At least one line more ends in \n - return '\n'; - } - - public static fromRawTextSource(rawTextSource: IRawTextSource, defaultEOL: DefaultEndOfLine): ITextSource { - return { - lines: rawTextSource.lines, - BOM: rawTextSource.BOM, - EOL: TextSource._getEOL(rawTextSource, defaultEOL), - containsRTL: rawTextSource.containsRTL, - isBasicASCII: rawTextSource.isBasicASCII, - }; - } -} diff --git a/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts b/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts index 13c81a70acb..360bbb4fa59 100644 --- a/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts +++ b/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts @@ -13,6 +13,7 @@ import { SearchData, isValidMatch, Searcher, createFindMatch } from 'vs/editor/c import { FindMatch } from 'vs/editor/common/model'; // const lfRegex = new RegExp(/\r\n|\r|\n/g); +export const AverageBufferSize = 65535; export function createUintArray(arr: number[]): Uint32Array | Uint16Array { let r; @@ -311,7 +312,7 @@ export class PieceTreeBase { } normalizeEOL(eol: '\r\n' | '\n') { - let averageBufferSize = 65536; + let averageBufferSize = AverageBufferSize; let min = averageBufferSize - Math.floor(averageBufferSize / 3); let max = min * 2; @@ -536,11 +537,23 @@ export class PieceTreeBase { public getLineCharCode(lineNumber: number, index: number): number { let nodePos = this.nodeAt2(lineNumber, index + 1); - let buffer = this._buffers[nodePos.node.piece.bufferIndex]; - let startOffset = this.offsetInBuffer(nodePos.node.piece.bufferIndex, nodePos.node.piece.start); - let targetOffset = startOffset + index; + if (nodePos.remainder === nodePos.node.piece.length) { + // the char we want to fetch is at the head of next node. + let matchingNode = nodePos.node.next(); + if (!matchingNode) { + return 0; + } - return buffer.buffer.charCodeAt(targetOffset); + let buffer = this._buffers[matchingNode.piece.bufferIndex]; + let startOffset = this.offsetInBuffer(matchingNode.piece.bufferIndex, matchingNode.piece.start); + return buffer.buffer.charCodeAt(startOffset); + } else { + let buffer = this._buffers[nodePos.node.piece.bufferIndex]; + let startOffset = this.offsetInBuffer(nodePos.node.piece.bufferIndex, nodePos.node.piece.start); + let targetOffset = startOffset + nodePos.remainder; + + return buffer.buffer.charCodeAt(targetOffset); + } } public getLineLength(lineNumber: number): number { @@ -712,7 +725,8 @@ export class PieceTreeBase { if (node.piece.bufferIndex === 0 && piece.end.line === this._lastChangeBufferPos.line && piece.end.column === this._lastChangeBufferPos.column && - (nodeStartOffset + piece.length === offset) + (nodeStartOffset + piece.length === offset) && + value.length < AverageBufferSize ) { // changed buffer this.appendToNode(node, value); @@ -769,19 +783,27 @@ export class PieceTreeBase { this.deleteNodeTail(node, insertPosInBuffer); } - let newPiece = this.createNewPiece(value); + let newPieces = this.createNewPieces(value); if (newRightPiece.length > 0) { this.rbInsertRight(node, newRightPiece); } - this.rbInsertRight(node, newPiece); + + let tmpNode = node; + for (let k = 0; k < newPieces.length; k++) { + tmpNode = this.rbInsertRight(tmpNode, newPieces[k]); + } this.deleteNodes(nodesToDel); } else { this.insertContentToNodeRight(value, node); } } else { // insert new node - let piece = this.createNewPiece(value); - this.rbInsertLeft(null, piece); + let pieces = this.createNewPieces(value); + let node = this.rbInsertLeft(null, pieces[0]); + + for (let k = 1; k < pieces.length; k++) { + node = this.rbInsertRight(node, pieces[k]); + } } // todo, this is too brutal. Total line feed count should be updated the same way as lf_left. @@ -887,8 +909,11 @@ export class PieceTreeBase { } } - let newPiece = this.createNewPiece(value); - let newNode = this.rbInsertLeft(node, newPiece); + let newPieces = this.createNewPieces(value); + let newNode = this.rbInsertLeft(node, newPieces[newPieces.length - 1]); + for (let k = newPieces.length - 2; k >= 0; k--) { + newNode = this.rbInsertLeft(newNode, newPieces[k]); + } this.validateCRLFWithPrevNode(newNode); this.deleteNodes(nodesToDel); } @@ -900,8 +925,14 @@ export class PieceTreeBase { value += '\n'; } - let newPiece = this.createNewPiece(value); - let newNode = this.rbInsertRight(node, newPiece); + let newPieces = this.createNewPieces(value); + let newNode = this.rbInsertRight(node, newPieces[0]); + let tmpNode = newNode; + + for (let k = 1; k < newPieces.length; k++) { + tmpNode = this.rbInsertRight(tmpNode, newPieces[k]); + } + this.validateCRLFWithPrevNode(newNode); } @@ -994,7 +1025,47 @@ export class PieceTreeBase { } } - createNewPiece(text: string): Piece { + createNewPieces(text: string): Piece[] { + if (text.length > AverageBufferSize) { + // the content is large, operations like substring, charCode becomes slow + // so here we split it into smaller chunks, just like what we did for CR/LF normalization + let newPieces = []; + while (text.length > AverageBufferSize) { + const lastChar = text.charCodeAt(AverageBufferSize - 1); + let splitText; + if (lastChar === CharCode.CarriageReturn || (lastChar >= 0xd800 && lastChar <= 0xdbff)) { + // last character is \r or a high surrogate => keep it back + splitText = text.substring(0, AverageBufferSize - 1); + text = text.substring(AverageBufferSize - 1); + } else { + splitText = text.substring(0, AverageBufferSize); + text = text.substring(AverageBufferSize); + } + + let lineStarts = createLineStartsFast(splitText); + newPieces.push(new Piece( + this._buffers.length, /* buffer index */ + { line: 0, column: 0 }, + { line: lineStarts.length - 1, column: splitText.length - lineStarts[lineStarts.length - 1] }, + lineStarts.length - 1, + splitText.length + )); + this._buffers.push(new StringBuffer(splitText, lineStarts)); + } + + let lineStarts = createLineStartsFast(text); + newPieces.push(new Piece( + this._buffers.length, /* buffer index */ + { line: 0, column: 0 }, + { line: lineStarts.length - 1, column: text.length - lineStarts[lineStarts.length - 1] }, + lineStarts.length - 1, + text.length + )); + this._buffers.push(new StringBuffer(text, lineStarts)); + + return newPieces; + } + let startOffset = this._buffers[0].buffer.length; const lineStarts = createLineStartsFast(text, false); @@ -1029,14 +1100,14 @@ export class PieceTreeBase { let endColumn = endOffset - this._buffers[0].lineStarts[endIndex]; let endPos = { line: endIndex, column: endColumn }; let newPiece = new Piece( - 0, + 0, /** todo */ start, endPos, this.getLineFeedCnt(0, start, endPos), endOffset - startOffset ); this._lastChangeBufferPos = endPos; - return newPiece; + return [newPiece]; } getLinesRawContent(): string { @@ -1519,8 +1590,8 @@ export class PieceTreeBase { } // create new piece which contains \r\n - let piece = this.createNewPiece('\r\n'); - this.rbInsertRight(prev, piece); + let pieces = this.createNewPieces('\r\n'); + this.rbInsertRight(prev, pieces[0]); // delete empty nodes for (let i = 0; i < nodesToDel.length; i++) { diff --git a/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.ts b/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.ts index 3695a9597ab..27416907f49 100644 --- a/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.ts +++ b/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.ts @@ -7,12 +7,26 @@ import { Range } from 'vs/editor/common/core/range'; import { Position } from 'vs/editor/common/core/position'; import * as strings from 'vs/base/common/strings'; -import { IValidatedEditOperation } from 'vs/editor/common/model/linesTextBuffer/linesTextBuffer'; import { PieceTreeBase, StringBuffer } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase'; -import { IIdentifiedSingleEditOperation, EndOfLinePreference, ITextBuffer, ApplyEditsResult, IInternalModelContentChange, FindMatch } from 'vs/editor/common/model'; +import { IIdentifiedSingleEditOperation, EndOfLinePreference, ITextBuffer, ApplyEditsResult, IInternalModelContentChange, FindMatch, ISingleEditOperationIdentifier } from 'vs/editor/common/model'; import { ITextSnapshot } from 'vs/platform/files/common/files'; import { SearchData } from 'vs/editor/common/model/textModelSearch'; +export interface IValidatedEditOperation { + sortIndex: number; + identifier: ISingleEditOperationIdentifier; + range: Range; + rangeOffset: number; + rangeLength: number; + lines: string[]; + forceMoveMarkers: boolean; + isAutoWhitespaceEdit: boolean; +} + +export interface IReverseSingleEditOperation extends IIdentifiedSingleEditOperation { + sortIndex: number; +} + export class PieceTreeTextBuffer implements ITextBuffer { private _pieceTree: PieceTreeBase; private _BOM: string; @@ -229,18 +243,20 @@ export class PieceTreeTextBuffer implements ITextBuffer { } } - let reverseOperations: IIdentifiedSingleEditOperation[] = []; + let reverseOperations: IReverseSingleEditOperation[] = []; for (let i = 0; i < operations.length; i++) { let op = operations[i]; let reverseRange = reverseRanges[i]; reverseOperations[i] = { + sortIndex: op.sortIndex, identifier: op.identifier, range: reverseRange, text: this.getValueInRange(op.range), forceMoveMarkers: op.forceMoveMarkers }; } + reverseOperations.sort((a, b) => a.sortIndex - b.sortIndex); this._mightContainRTL = mightContainRTL; this._mightContainNonBasicASCII = mightContainNonBasicASCII; @@ -279,9 +295,9 @@ export class PieceTreeTextBuffer implements ITextBuffer { } /** - * Transform operations such that they represent the same logic edit, - * but that they also do not cause OOM crashes. - */ + * Transform operations such that they represent the same logic edit, + * but that they also do not cause OOM crashes. + */ private _reduceOperations(operations: IValidatedEditOperation[]): IValidatedEditOperation[] { if (operations.length < 1000) { // We know from empirical testing that a thousand edits work fine regardless of their shape. diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index ccc986b4d0f..3f50b4cf63e 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -33,28 +33,10 @@ import { EDITOR_MODEL_DEFAULTS } from 'vs/editor/common/config/editorOptions'; import { TextModelSearch, SearchParams, SearchData } from 'vs/editor/common/model/textModelSearch'; import { TPromise } from 'vs/base/common/winjs.base'; import { IStringStream, ITextSnapshot } from 'vs/platform/files/common/files'; -import { LinesTextBufferBuilder } from 'vs/editor/common/model/linesTextBuffer/linesTextBufferBuilder'; import { PieceTreeTextBufferBuilder } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBufferBuilder'; -import { ChunksTextBufferBuilder } from 'vs/editor/common/model/chunksTextBuffer/chunksTextBufferBuilder'; - -export enum TextBufferType { - LinesArray, - PieceTree, - Chunks -} -// Here is the master switch for the text buffer implementation: -export const OPTIONS = { - TEXT_BUFFER_IMPLEMENTATION: TextBufferType.PieceTree -}; function createTextBufferBuilder() { - if (OPTIONS.TEXT_BUFFER_IMPLEMENTATION === TextBufferType.PieceTree) { - return new PieceTreeTextBufferBuilder(); - } - if (OPTIONS.TEXT_BUFFER_IMPLEMENTATION === TextBufferType.Chunks) { - return new ChunksTextBufferBuilder(); - } - return new LinesTextBufferBuilder(); + return new PieceTreeTextBufferBuilder(); } export function createTextBufferFactory(text: string): model.ITextBufferFactory { @@ -173,8 +155,6 @@ class TextModelSnapshot implements ITextSnapshot { export class TextModel extends Disposable implements model.ITextModel { private static readonly MODEL_SYNC_LIMIT = 50 * 1024 * 1024; // 50 MB - private static readonly MODEL_TOKENIZATION_LIMIT = 20 * 1024 * 1024; // 20 MB - private static readonly MANY_MANY_LINES = 300 * 1000; // 300K lines public static DEFAULT_CREATION_OPTIONS: model.ITextModelCreationOptions = { isForSimpleWidget: false, @@ -183,6 +163,8 @@ export class TextModel extends Disposable implements model.ITextModel { detectIndentation: false, defaultEOL: model.DefaultEndOfLine.LF, trimAutoWhitespace: EDITOR_MODEL_DEFAULTS.trimAutoWhitespace, + largeFileSize: EDITOR_MODEL_DEFAULTS.largeFileSize, + largeFileLineCount: EDITOR_MODEL_DEFAULTS.largeFileLineCount, }; public static createFromString(text: string, options: model.ITextModelCreationOptions = TextModel.DEFAULT_CREATION_OPTIONS, languageIdentifier: LanguageIdentifier = null, uri: URI = null): TextModel { @@ -229,11 +211,14 @@ export class TextModel extends Disposable implements model.ITextModel { public readonly onDidChangeOptions: Event = this._onDidChangeOptions.event; private readonly _eventEmitter: DidChangeContentEmitter = this._register(new DidChangeContentEmitter()); + public onDidChangeRawContentFast(listener: (e: ModelRawContentChangedEvent) => void): IDisposable { + return this._eventEmitter.fastEvent((e: InternalModelContentChangeEvent) => listener(e.rawContentChangedEvent)); + } public onDidChangeRawContent(listener: (e: ModelRawContentChangedEvent) => void): IDisposable { - return this._eventEmitter.event((e: InternalModelContentChangeEvent) => listener(e.rawContentChangedEvent)); + return this._eventEmitter.slowEvent((e: InternalModelContentChangeEvent) => listener(e.rawContentChangedEvent)); } public onDidChangeContent(listener: (e: IModelContentChangedEvent) => void): IDisposable { - return this._eventEmitter.event((e: InternalModelContentChangeEvent) => listener(e.contentChangedEvent)); + return this._eventEmitter.slowEvent((e: InternalModelContentChangeEvent) => listener(e.contentChangedEvent)); } //#endregion @@ -304,8 +289,8 @@ export class TextModel extends Disposable implements model.ITextModel { // If a model is too large at construction time, it will never get tokenized, // under no circumstances. this._isTooLargeForTokenization = ( - (bufferTextLength > TextModel.MODEL_TOKENIZATION_LIMIT) - || (bufferLineCount > TextModel.MANY_MANY_LINES) + (bufferTextLength > creationOptions.largeFileSize) + || (bufferLineCount > creationOptions.largeFileLineCount) ); this._shouldSimplifyMode = ( @@ -401,10 +386,11 @@ export class TextModel extends Disposable implements model.ITextModel { this.setValueFromTextBuffer(textBuffer); } - private _createContentChanged2(startLineNumber: number, startColumn: number, endLineNumber: number, endColumn: number, rangeLength: number, text: string, isUndoing: boolean, isRedoing: boolean, isFlush: boolean): IModelContentChangedEvent { + private _createContentChanged2(range: Range, rangeOffset: number, rangeLength: number, text: string, isUndoing: boolean, isRedoing: boolean, isFlush: boolean): IModelContentChangedEvent { return { changes: [{ - range: new Range(startLineNumber, startColumn, endLineNumber, endColumn), + range: range, + rangeOffset: rangeOffset, rangeLength: rangeLength, text: text, }], @@ -450,7 +436,7 @@ export class TextModel extends Disposable implements model.ITextModel { false, false ), - this._createContentChanged2(1, 1, endLineNumber, endColumn, oldModelValueLength, this.getValue(), false, false, true) + this._createContentChanged2(new Range(1, 1, endLineNumber, endColumn), 0, oldModelValueLength, this.getValue(), false, false, true) ); } @@ -481,7 +467,7 @@ export class TextModel extends Disposable implements model.ITextModel { false, false ), - this._createContentChanged2(1, 1, endLineNumber, endColumn, oldModelValueLength, this.getValue(), false, false, false) + this._createContentChanged2(new Range(1, 1, endLineNumber, endColumn), 0, oldModelValueLength, this.getValue(), false, false, false) ); } @@ -551,6 +537,10 @@ export class TextModel extends Disposable implements model.ITextModel { return this._attachedEditorCount > 0; } + public getAttachedEditorCount(): number { + return this._attachedEditorCount; + } + public isTooLargeForHavingARichMode(): boolean { return this._shouldSimplifyMode; } @@ -782,6 +772,15 @@ export class TextModel extends Disposable implements model.ITextModel { return this._buffer.getLineContent(lineNumber); } + public getLineLength(lineNumber: number): number { + this._assertNotDisposed(); + if (lineNumber < 1 || lineNumber > this.getLineCount()) { + throw new Error('Illegal value for lineNumber'); + } + + return this._buffer.getLineLength(lineNumber); + } + public getLinesContent(): string[] { this._assertNotDisposed(); return this._buffer.getLinesContent(); @@ -892,6 +891,41 @@ export class TextModel extends Disposable implements model.ITextModel { return new Range(startLineNumber, startColumn, endLineNumber, endColumn); } + /** + * @param strict Do NOT allow a position inside a high-low surrogate pair + */ + private _isValidPosition(lineNumber: number, column: number, strict: boolean): boolean { + + if (lineNumber < 1) { + return false; + } + + const lineCount = this._buffer.getLineCount(); + if (lineNumber > lineCount) { + return false; + } + + if (column < 1) { + return false; + } + + const maxColumn = this.getLineMaxColumn(lineNumber); + if (column > maxColumn) { + return false; + } + + if (strict) { + if (column > 1) { + const charCodeBefore = this._buffer.getLineCharCode(lineNumber, column - 2); + if (strings.isHighSurrogate(charCodeBefore)) { + return false; + } + } + } + + return true; + } + /** * @param strict Do NOT allow a position inside a high-low surrogate pair */ @@ -932,11 +966,60 @@ export class TextModel extends Disposable implements model.ITextModel { public validatePosition(position: IPosition): Position { this._assertNotDisposed(); + + // Avoid object allocation and cover most likely case + if (position instanceof Position) { + if (this._isValidPosition(position.lineNumber, position.column, true)) { + return position; + } + } + return this._validatePosition(position.lineNumber, position.column, true); } + /** + * @param strict Do NOT allow a range to have its boundaries inside a high-low surrogate pair + */ + private _isValidRange(range: Range, strict: boolean): boolean { + const startLineNumber = range.startLineNumber; + const startColumn = range.startColumn; + const endLineNumber = range.endLineNumber; + const endColumn = range.endColumn; + + if (!this._isValidPosition(startLineNumber, startColumn, false)) { + return false; + } + if (!this._isValidPosition(endLineNumber, endColumn, false)) { + return false; + } + + if (strict) { + const charCodeBeforeStart = (startColumn > 1 ? this._buffer.getLineCharCode(startLineNumber, startColumn - 2) : 0); + const charCodeBeforeEnd = (endColumn > 1 && endColumn <= this._buffer.getLineLength(endLineNumber) ? this._buffer.getLineCharCode(endLineNumber, endColumn - 2) : 0); + + const startInsideSurrogatePair = strings.isHighSurrogate(charCodeBeforeStart); + const endInsideSurrogatePair = strings.isHighSurrogate(charCodeBeforeEnd); + + if (!startInsideSurrogatePair && !endInsideSurrogatePair) { + return true; + } + + return false; + } + + return true; + } + public validateRange(_range: IRange): Range { this._assertNotDisposed(); + + // Avoid object allocation and cover most likely case + if ((_range instanceof Range) && !(_range instanceof Selection)) { + if (this._isValidRange(_range, true)) { + return _range; + } + } + const start = this._validatePosition(_range.startLineNumber, _range.startColumn, false); const end = this._validatePosition(_range.endLineNumber, _range.endColumn, false); @@ -1000,7 +1083,7 @@ export class TextModel extends Disposable implements model.ITextModel { searchRange = this.getFullModelRange(); } - if (!isRegex && searchString.indexOf('\n') < 0 && OPTIONS.TEXT_BUFFER_IMPLEMENTATION === TextBufferType.PieceTree) { + if (!isRegex && searchString.indexOf('\n') < 0) { // not regex, not multi line const searchParams = new SearchParams(searchString, isRegex, matchCase, wordSeparators); const searchData = searchParams.parseSearchRequest(); @@ -1019,7 +1102,7 @@ export class TextModel extends Disposable implements model.ITextModel { this._assertNotDisposed(); const searchStart = this.validatePosition(rawSearchStart); - if (!isRegex && searchString.indexOf('\n') < 0 && OPTIONS.TEXT_BUFFER_IMPLEMENTATION === TextBufferType.PieceTree) { + if (!isRegex && searchString.indexOf('\n') < 0) { const searchParams = new SearchParams(searchString, isRegex, matchCase, wordSeparators); const searchData = searchParams.parseSearchRequest(); const lineCount = this.getLineCount(); @@ -1686,8 +1769,15 @@ export class TextModel extends Disposable implements model.ITextModel { let text = this.getLineContent(i); let r = this._tokens._tokenizeText(this._buffer, text, state); if (r) { - state = r.endState.clone(); this._tokens._setTokens(this._tokens.languageIdentifier.id, i - 1, text.length, r.tokens); + /* + * we think it's valid and give it a state but we don't update `_invalidLineStartIndex` then the top-to-bottom tokenization + * goes through the viewport, it can skip them if they already have correct tokens and state, and the lines after the viewport + * can still be tokenized. + */ + this._tokens._setIsInvalid(i - 1, false); + this._tokens._setState(i - 1, state); + state = r.endState.clone(); eventBuilder.registerChangedTokens(i); } else { state = initialState.clone(); @@ -2021,6 +2111,10 @@ export class TextModel extends Disposable implements model.ITextModel { } private _matchFoundBracket(foundBracket: Range, data: RichEditBracket, isOpen: boolean): [Range, Range] { + if (!data) { + return null; + } + if (isOpen) { let matched = this._findMatchingBracketDown(data, foundBracket.getEndPosition()); if (matched) { @@ -2300,6 +2394,168 @@ export class TextModel extends Disposable implements model.ITextModel { return TextModel.computeIndentLevel(this._buffer.getLineContent(lineIndex + 1), this._options.tabSize); } + public getActiveIndentGuide(lineNumber: number): model.IActiveIndentGuideInfo { + this._assertNotDisposed(); + const lineCount = this.getLineCount(); + + if (lineNumber < 1 || lineNumber > lineCount) { + throw new Error('Illegal value for lineNumber'); + } + + const foldingRules = LanguageConfigurationRegistry.getFoldingRules(this._languageIdentifier.id); + const offSide = foldingRules && foldingRules.offSide; + + let up_aboveContentLineIndex = -2; /* -2 is a marker for not having computed it */ + let up_aboveContentLineIndent = -1; + let up_belowContentLineIndex = -2; /* -2 is a marker for not having computed it */ + let up_belowContentLineIndent = -1; + const up_resolveIndents = (lineNumber: number) => { + if (up_aboveContentLineIndex !== -1 && (up_aboveContentLineIndex === -2 || up_aboveContentLineIndex > lineNumber - 1)) { + up_aboveContentLineIndex = -1; + up_aboveContentLineIndent = -1; + + // must find previous line with content + for (let lineIndex = lineNumber - 2; lineIndex >= 0; lineIndex--) { + let indent = this._computeIndentLevel(lineIndex); + if (indent >= 0) { + up_aboveContentLineIndex = lineIndex; + up_aboveContentLineIndent = indent; + break; + } + } + } + + if (up_belowContentLineIndex === -2) { + up_belowContentLineIndex = -1; + up_belowContentLineIndent = -1; + + // must find next line with content + for (let lineIndex = lineNumber; lineIndex < lineCount; lineIndex++) { + let indent = this._computeIndentLevel(lineIndex); + if (indent >= 0) { + up_belowContentLineIndex = lineIndex; + up_belowContentLineIndent = indent; + break; + } + } + } + }; + + let down_aboveContentLineIndex = -2; /* -2 is a marker for not having computed it */ + let down_aboveContentLineIndent = -1; + let down_belowContentLineIndex = -2; /* -2 is a marker for not having computed it */ + let down_belowContentLineIndent = -1; + const down_resolveIndents = (lineNumber: number) => { + if (down_aboveContentLineIndex === -2) { + down_aboveContentLineIndex = -1; + down_aboveContentLineIndent = -1; + + // must find previous line with content + for (let lineIndex = lineNumber - 2; lineIndex >= 0; lineIndex--) { + let indent = this._computeIndentLevel(lineIndex); + if (indent >= 0) { + down_aboveContentLineIndex = lineIndex; + down_aboveContentLineIndent = indent; + break; + } + } + } + + if (down_belowContentLineIndex !== -1 && (down_belowContentLineIndex === -2 || down_belowContentLineIndex < lineNumber - 1)) { + down_belowContentLineIndex = -1; + down_belowContentLineIndent = -1; + + // must find next line with content + for (let lineIndex = lineNumber; lineIndex < lineCount; lineIndex++) { + let indent = this._computeIndentLevel(lineIndex); + if (indent >= 0) { + down_belowContentLineIndex = lineIndex; + down_belowContentLineIndent = indent; + break; + } + } + } + }; + + let startLineNumber = 0; + let goUp = true; + let endLineNumber = 0; + let goDown = true; + let indent = 0; + + for (let distance = 0; goUp || goDown; distance++) { + const upLineNumber = lineNumber - distance; + const downLineNumber = lineNumber + distance; + + if (upLineNumber < 1) { + goUp = false; + } + if (downLineNumber > lineCount) { + goDown = false; + } + + if (goUp) { + // compute indent level going up + let upLineIndentLevel: number; + + const currentIndent = this._computeIndentLevel(upLineNumber - 1); + if (currentIndent >= 0) { + // This line has content (besides whitespace) + // Use the line's indent + up_belowContentLineIndex = upLineNumber - 1; + up_belowContentLineIndent = currentIndent; + upLineIndentLevel = Math.ceil(currentIndent / this._options.tabSize); + } else { + up_resolveIndents(upLineNumber); + upLineIndentLevel = this._getIndentLevelForWhitespaceLine(offSide, up_aboveContentLineIndent, up_belowContentLineIndent); + } + + if (distance === 0) { + // This is the initial line number + startLineNumber = upLineNumber; + endLineNumber = downLineNumber; + indent = upLineIndentLevel; + if (indent === 0) { + // No need to continue + return { startLineNumber, endLineNumber, indent }; + } + continue; + } + + if (upLineIndentLevel >= indent) { + startLineNumber = upLineNumber; + } else { + goUp = false; + } + } + + if (goDown) { + // compute indent level going down + let downLineIndentLevel: number; + + const currentIndent = this._computeIndentLevel(downLineNumber - 1); + if (currentIndent >= 0) { + // This line has content (besides whitespace) + // Use the line's indent + down_aboveContentLineIndex = downLineNumber - 1; + down_aboveContentLineIndent = currentIndent; + downLineIndentLevel = Math.ceil(currentIndent / this._options.tabSize); + } else { + down_resolveIndents(downLineNumber); + downLineIndentLevel = this._getIndentLevelForWhitespaceLine(offSide, down_aboveContentLineIndent, down_belowContentLineIndent); + } + + if (downLineIndentLevel >= indent) { + endLineNumber = downLineNumber; + } else { + goDown = false; + } + } + } + + return { startLineNumber, endLineNumber, indent }; + } + public getLinesIndentGuides(startLineNumber: number, endLineNumber: number): number[] { this._assertNotDisposed(); const lineCount = this.getLineCount(); @@ -2365,32 +2621,38 @@ export class TextModel extends Disposable implements model.ITextModel { } } - if (aboveContentLineIndent === -1 || belowContentLineIndent === -1) { - // At the top or bottom of the file - result[resultIndex] = 0; + result[resultIndex] = this._getIndentLevelForWhitespaceLine(offSide, aboveContentLineIndent, belowContentLineIndent); - } else if (aboveContentLineIndent < belowContentLineIndent) { - // we are inside the region above - result[resultIndex] = (1 + Math.floor(aboveContentLineIndent / this._options.tabSize)); - - } else if (aboveContentLineIndent === belowContentLineIndent) { - // we are in between two regions - result[resultIndex] = Math.ceil(belowContentLineIndent / this._options.tabSize); - - } else { - - if (offSide) { - // same level as region below - result[resultIndex] = Math.ceil(belowContentLineIndent / this._options.tabSize); - } else { - // we are inside the region that ends below - result[resultIndex] = (1 + Math.floor(belowContentLineIndent / this._options.tabSize)); - } - - } } return result; } + + private _getIndentLevelForWhitespaceLine(offSide: boolean, aboveContentLineIndent: number, belowContentLineIndent: number): number { + if (aboveContentLineIndent === -1 || belowContentLineIndent === -1) { + // At the top or bottom of the file + return 0; + + } else if (aboveContentLineIndent < belowContentLineIndent) { + // we are inside the region above + return (1 + Math.floor(aboveContentLineIndent / this._options.tabSize)); + + } else if (aboveContentLineIndent === belowContentLineIndent) { + // we are in between two regions + return Math.ceil(belowContentLineIndent / this._options.tabSize); + + } else { + + if (offSide) { + // same level as region below + return Math.ceil(belowContentLineIndent / this._options.tabSize); + } else { + // we are inside the region that ends below + return (1 + Math.floor(belowContentLineIndent / this._options.tabSize)); + } + + } + } + //#endregion } @@ -2505,22 +2767,20 @@ export class ModelDecorationOverviewRulerOptions implements model.IModelDecorati } } -let lastStaticId = 0; - export class ModelDecorationOptions implements model.IModelDecorationOptions { public static EMPTY: ModelDecorationOptions; public static register(options: model.IModelDecorationOptions): ModelDecorationOptions { - return new ModelDecorationOptions(++lastStaticId, options); + return new ModelDecorationOptions(options); } public static createDynamic(options: model.IModelDecorationOptions): ModelDecorationOptions { - return new ModelDecorationOptions(0, options); + return new ModelDecorationOptions(options); } - readonly staticId: number; readonly stickiness: model.TrackedRangeStickiness; + readonly zIndex: number; readonly className: string; readonly hoverMessage: IMarkdownString | IMarkdownString[]; readonly glyphMarginHoverMessage: IMarkdownString | IMarkdownString[]; @@ -2531,12 +2791,13 @@ export class ModelDecorationOptions implements model.IModelDecorationOptions { readonly linesDecorationsClassName: string; readonly marginClassName: string; readonly inlineClassName: string; + readonly inlineClassNameAffectsLetterSpacing: boolean; readonly beforeContentClassName: string; readonly afterContentClassName: string; - private constructor(staticId: number, options: model.IModelDecorationOptions) { - this.staticId = staticId; + private constructor(options: model.IModelDecorationOptions) { this.stickiness = options.stickiness || model.TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges; + this.zIndex = options.zIndex || 0; this.className = options.className ? cleanClassName(options.className) : strings.empty; this.hoverMessage = options.hoverMessage || []; this.glyphMarginHoverMessage = options.glyphMarginHoverMessage || []; @@ -2547,6 +2808,7 @@ export class ModelDecorationOptions implements model.IModelDecorationOptions { this.linesDecorationsClassName = options.linesDecorationsClassName ? cleanClassName(options.linesDecorationsClassName) : strings.empty; this.marginClassName = options.marginClassName ? cleanClassName(options.marginClassName) : strings.empty; this.inlineClassName = options.inlineClassName ? cleanClassName(options.inlineClassName) : strings.empty; + this.inlineClassNameAffectsLetterSpacing = options.inlineClassNameAffectsLetterSpacing || false; this.beforeContentClassName = options.beforeContentClassName ? cleanClassName(options.beforeContentClassName) : strings.empty; this.afterContentClassName = options.afterContentClassName ? cleanClassName(options.afterContentClassName) : strings.empty; } @@ -2607,8 +2869,13 @@ export class DidChangeDecorationsEmitter extends Disposable { export class DidChangeContentEmitter extends Disposable { - private readonly _actual: Emitter = this._register(new Emitter()); - public readonly event: Event = this._actual.event; + /** + * Both `fastEvent` and `slowEvent` work the same way and contain the same events, but first we invoke `fastEvent` and then `slowEvent`. + */ + private readonly _fastEmitter: Emitter = this._register(new Emitter()); + public readonly fastEvent: Event = this._fastEmitter.event; + private readonly _slowEmitter: Emitter = this._register(new Emitter()); + public readonly slowEvent: Event = this._slowEmitter.event; private _deferredCnt: number; private _deferredEvent: InternalModelContentChangeEvent; @@ -2629,7 +2896,8 @@ export class DidChangeContentEmitter extends Disposable { if (this._deferredEvent !== null) { const e = this._deferredEvent; this._deferredEvent = null; - this._actual.fire(e); + this._fastEmitter.fire(e); + this._slowEmitter.fire(e); } } } @@ -2643,6 +2911,7 @@ export class DidChangeContentEmitter extends Disposable { } return; } - this._actual.fire(e); + this._fastEmitter.fire(e); + this._slowEmitter.fire(e); } } diff --git a/src/vs/editor/common/model/textModelEvents.ts b/src/vs/editor/common/model/textModelEvents.ts index f65fdca1ef2..d6593abd1cb 100644 --- a/src/vs/editor/common/model/textModelEvents.ts +++ b/src/vs/editor/common/model/textModelEvents.ts @@ -32,6 +32,10 @@ export interface IModelContentChange { * The range that got replaced. */ readonly range: IRange; + /** + * The offset of the range that got replaced. + */ + readonly rangeOffset: number; /** * The length of the range that got replaced. */ diff --git a/src/vs/editor/common/model/textModelTokens.ts b/src/vs/editor/common/model/textModelTokens.ts index 80e5e1c7287..ab4e221927f 100644 --- a/src/vs/editor/common/model/textModelTokens.ts +++ b/src/vs/editor/common/model/textModelTokens.ts @@ -233,7 +233,7 @@ export class ModelLinesTokens { } } - private _setIsInvalid(lineIndex: number, invalid: boolean): void { + _setIsInvalid(lineIndex: number, invalid: boolean): void { if (lineIndex < this._tokens.length && this._tokens[lineIndex]) { this._tokens[lineIndex]._invalid = invalid; } @@ -278,7 +278,7 @@ export class ModelLinesTokens { target._lineTokens = tokens.buffer; } - private _setState(lineIndex: number, state: IState): void { + _setState(lineIndex: number, state: IState): void { if (lineIndex < this._tokens.length && this._tokens[lineIndex]) { this._tokens[lineIndex]._state = state; } else { diff --git a/src/vs/editor/common/modes.ts b/src/vs/editor/common/modes.ts index 6d7f7468e84..e9caa888ae2 100644 --- a/src/vs/editor/common/modes.ts +++ b/src/vs/editor/common/modes.ts @@ -230,7 +230,7 @@ export interface Hover { * editor will use the range at the current position or the * current position itself. */ - range: IRange; + range?: IRange; } /** @@ -364,6 +364,11 @@ export interface CodeActionProvider { * Provide commands for the given document and range. */ provideCodeActions(model: model.ITextModel, range: Range, context: CodeActionContext, token: CancellationToken): CodeAction[] | Thenable; + + /** + * Optional list of of CodeActionKinds that this provider returns. + */ + providedCodeActionKinds?: string[]; } /** @@ -825,73 +830,60 @@ export interface DocumentColorProvider { */ provideColorPresentations(model: model.ITextModel, colorInfo: IColorInformation, token: CancellationToken): IColorPresentation[] | Thenable; } - -/** - * @internal - */ export interface FoldingContext { - maxRanges?: number; } - /** * A provider of colors for editor models. */ -/** - * @internal - */ -export interface FoldingProvider { +export interface FoldingRangeProvider { /** * Provides the color ranges for a specific model. */ - provideFoldingRanges(model: model.ITextModel, context: FoldingContext, token: CancellationToken): IFoldingRangeList | Thenable; + provideFoldingRanges(model: model.ITextModel, context: FoldingContext, token: CancellationToken): FoldingRange[] | Thenable; } -/** - * @internal - */ -export interface IFoldingRangeList { - ranges: IFoldingRange[]; +export interface FoldingRange { + + /** + * The zero-based start line of the range to fold. The folded area starts after the line's last character. + */ + start: number; + + /** + * The zero-based end line of the range to fold. The folded area ends with the line's last character. + */ + end: number; + + /** + * Describes the [Kind](#FoldingRangeKind) of the folding range such as [Comment](#FoldingRangeKind.Comment) or + * [Region](#FoldingRangeKind.Region). The kind is used to categorize folding ranges and used by commands + * like 'Fold all comments'. See + * [FoldingRangeKind](#FoldingRangeKind) for an enumeration of standardized kinds. + */ + kind?: FoldingRangeKind; } -/** - * @internal - */ -export interface IFoldingRange { +export class FoldingRangeKind { + /** + * Kind for folding range representing a comment. The value of the kind is 'comment'. + */ + static readonly Comment = new FoldingRangeKind('comment'); + /** + * Kind for folding range representing a import. The value of the kind is 'imports'. + */ + static readonly Imports = new FoldingRangeKind('imports'); + /** + * Kind for folding range representing regions (for example marked by `#region`, `#endregion`). + * The value of the kind is 'region'. + */ + static readonly Region = new FoldingRangeKind('region'); /** - * The start line number + * Creates a new [FoldingRangeKind](#FoldingRangeKind). + * + * @param value of the kind. */ - startLineNumber: number; - - /** - * The end line number - */ - endLineNumber: number; - - /** - * The optional type of the folding range - */ - type?: FoldingRangeType | string; - - // auto-collapse - // header span - -} -/** - * @internal - */ -export enum FoldingRangeType { - /** - * Folding range for a comment - */ - Comment = 'comment', - /** - * Folding range for a imports or includes - */ - Imports = 'imports', - /** - * Folding range for a region (e.g. `#region`) - */ - Region = 'region' + public constructor(public value: string) { + } } /** @@ -924,15 +916,14 @@ export interface WorkspaceEdit { rejectReason?: string; // TODO@joh, move to rename } -export interface RenameContext { +export interface RenameLocation { range: IRange; text: string; - message?: string; } export interface RenameProvider { provideRenameEdits(model: model.ITextModel, position: Position, newName: string, token: CancellationToken): WorkspaceEdit | Thenable; - resolveRenameLocation?(model: model.ITextModel, position: Position, token: CancellationToken): RenameContext | Thenable; + resolveRenameLocation?(model: model.ITextModel, position: Position, token: CancellationToken): RenameLocation | Thenable; } @@ -1043,7 +1034,7 @@ export const ColorProviderRegistry = new LanguageFeatureRegistry(); +export const FoldingRangeProviderRegistry = new LanguageFeatureRegistry(); /** * @internal diff --git a/src/vs/editor/common/services/editorSimpleWorker.ts b/src/vs/editor/common/services/editorSimpleWorker.ts index b7302571032..046b0f32902 100644 --- a/src/vs/editor/common/services/editorSimpleWorker.ts +++ b/src/vs/editor/common/services/editorSimpleWorker.ts @@ -22,6 +22,7 @@ import { getWordAtText, ensureValidWordDefinition } from 'vs/editor/common/model import { createMonacoBaseAPI } from 'vs/editor/common/standalone/standaloneBase'; import { IWordAtPosition, EndOfLineSequence } from 'vs/editor/common/model'; import { globals } from 'vs/base/common/platform'; +import { IIterator } from 'vs/base/common/iterator'; export interface IMirrorModel { readonly uri: URI; @@ -58,8 +59,8 @@ export interface ICommonModel { getLinesContent(): string[]; getLineCount(): number; getLineContent(lineNumber: number): string; + createWordIterator(wordDefinition: RegExp): IIterator; getWordUntilPosition(position: IPosition, wordDefinition: RegExp): IWordAtPosition; - getAllUniqueWords(wordDefinition: RegExp, skipWordOnce?: string): string[]; getValueInRange(range: IRange): string; getWordAtPosition(position: IPosition, wordDefinition: RegExp): Range; offsetAt(position: IPosition): number; @@ -146,30 +147,37 @@ class MirrorModel extends BaseMirrorModel implements ICommonModel { }; } - private _getAllWords(wordDefinition: RegExp): string[] { - let result: string[] = []; - this._lines.forEach((line) => { - this._wordenize(line, wordDefinition).forEach((info) => { - result.push(line.substring(info.start, info.end)); - }); - }); - return result; - } + public createWordIterator(wordDefinition: RegExp): IIterator { + let obj = { + done: false, + value: '' + }; + let lineNumber = 0; + let lineText: string; + let wordRangesIdx = 0; + let wordRanges: IWordRange[] = []; + let next = () => { + + if (wordRangesIdx < wordRanges.length) { + obj.done = false; + obj.value = lineText.substring(wordRanges[wordRangesIdx].start, wordRanges[wordRangesIdx].end); + wordRangesIdx += 1; + + } else if (lineNumber >= this._lines.length) { + obj.done = true; + obj.value = undefined; - public getAllUniqueWords(wordDefinition: RegExp, skipWordOnce?: string): string[] { - let foundSkipWord = false; - let uniqueWords = Object.create(null); - return this._getAllWords(wordDefinition).filter((word) => { - if (skipWordOnce && !foundSkipWord && skipWordOnce === word) { - foundSkipWord = true; - return false; - } else if (uniqueWords[word]) { - return false; } else { - uniqueWords[word] = true; - return true; + lineText = this._lines[lineNumber]; + wordRanges = this._wordenize(lineText, wordDefinition); + wordRangesIdx = 0; + lineNumber += 1; + return next(); } - }); + + return obj; + }; + return { next }; } private _wordenize(content: string, wordDefinition: RegExp): IWordRange[] { @@ -427,6 +435,8 @@ export abstract class BaseEditorSimpleWorker { // ---- BEGIN suggest -------------------------------------------------------------------------- + private static readonly _suggestionsLimit = 10000; + public textualSuggest(modelUrl: string, position: IPosition, wordDef: string, wordDefFlags: string): TPromise { const model = this._getModel(modelUrl); if (model) { @@ -434,17 +444,32 @@ export abstract class BaseEditorSimpleWorker { const wordDefRegExp = new RegExp(wordDef, wordDefFlags); const currentWord = model.getWordUntilPosition(position, wordDefRegExp).word; - for (const word of model.getAllUniqueWords(wordDefRegExp)) { - if (word !== currentWord && isNaN(Number(word))) { - suggestions.push({ - type: 'text', - label: word, - insertText: word, - noAutoAccept: true, - overwriteBefore: currentWord.length - }); + const seen: Record = Object.create(null); + seen[currentWord] = true; + + for ( + let iter = model.createWordIterator(wordDefRegExp), e = iter.next(); + !e.done && suggestions.length <= BaseEditorSimpleWorker._suggestionsLimit; + e = iter.next() + ) { + const word = e.value; + if (seen[word]) { + continue; } + seen[word] = true; + if (!isNaN(Number(word))) { + continue; + } + + suggestions.push({ + type: 'text', + label: word, + insertText: word, + noAutoAccept: true, + overwriteBefore: currentWord.length + }); } + return TPromise.as({ suggestions }); } return undefined; diff --git a/src/vs/editor/common/services/languagesRegistry.ts b/src/vs/editor/common/services/languagesRegistry.ts index 546aa1bbc17..13225e51b8c 100644 --- a/src/vs/editor/common/services/languagesRegistry.ts +++ b/src/vs/editor/common/services/languagesRegistry.ts @@ -111,13 +111,9 @@ export class LanguagesRegistry { let primaryMime: string = null; - if (typeof lang.mimetypes !== 'undefined' && Array.isArray(lang.mimetypes)) { - for (let i = 0; i < lang.mimetypes.length; i++) { - if (!primaryMime) { - primaryMime = lang.mimetypes[i]; - } - resolvedLanguage.mimetypes.push(lang.mimetypes[i]); - } + if (Array.isArray(lang.mimetypes) && lang.mimetypes.length > 0) { + resolvedLanguage.mimetypes.push(...lang.mimetypes); + primaryMime = lang.mimetypes[0]; } if (!primaryMime) { diff --git a/src/vs/editor/common/services/modelServiceImpl.ts b/src/vs/editor/common/services/modelServiceImpl.ts index b2dbb1d5720..6490f6d7a0e 100644 --- a/src/vs/editor/common/services/modelServiceImpl.ts +++ b/src/vs/editor/common/services/modelServiceImpl.ts @@ -13,7 +13,6 @@ import URI from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import { IMarker, IMarkerService, MarkerSeverity } from 'vs/platform/markers/common/markers'; import { Range } from 'vs/editor/common/core/range'; -import { Selection } from 'vs/editor/common/core/selection'; import { TextModel, createTextBuffer } from 'vs/editor/common/model/textModel'; import { IMode, LanguageIdentifier } from 'vs/editor/common/modes'; import { IModelService } from 'vs/editor/common/services/modelService'; @@ -83,15 +82,23 @@ class ModelMarkerHandler { } private static _createDecorationRange(model: ITextModel, rawMarker: IMarker): Range { - let marker = model.validateRange(new Range(rawMarker.startLineNumber, rawMarker.startColumn, rawMarker.endLineNumber, rawMarker.endColumn)); - let ret: Range = new Range(marker.startLineNumber, marker.startColumn, marker.endLineNumber, marker.endColumn); + + let ret = Range.lift(rawMarker); + + if (rawMarker.severity === MarkerSeverity.Hint && Range.spansMultipleLines(ret)) { + // never render hints on multiple lines + ret = ret.setEndPosition(ret.startLineNumber, ret.startColumn); + } + + ret = model.validateRange(ret); + if (ret.isEmpty()) { let word = model.getWordAtPosition(ret.getStartPosition()); if (word) { ret = new Range(ret.startLineNumber, word.startColumn, ret.endLineNumber, word.endColumn); } else { - let maxColumn = model.getLineLastNonWhitespaceColumn(marker.startLineNumber) || - model.getLineMaxColumn(marker.startLineNumber); + let maxColumn = model.getLineLastNonWhitespaceColumn(ret.startLineNumber) || + model.getLineMaxColumn(ret.startLineNumber); if (maxColumn === 1) { // empty line @@ -119,26 +126,31 @@ class ModelMarkerHandler { let className: string; let color: ThemeColor; let darkColor: ThemeColor; + let zIndex: number; switch (marker.severity) { case MarkerSeverity.Hint: className = ClassName.EditorHintDecoration; + zIndex = 0; break; case MarkerSeverity.Warning: className = ClassName.EditorWarningDecoration; color = themeColorFromId(overviewRulerWarning); darkColor = themeColorFromId(overviewRulerWarning); + zIndex = 20; break; case MarkerSeverity.Info: className = ClassName.EditorInfoDecoration; color = themeColorFromId(overviewRulerInfo); darkColor = themeColorFromId(overviewRulerInfo); + zIndex = 10; break; case MarkerSeverity.Error: default: className = ClassName.EditorErrorDecoration; color = themeColorFromId(overviewRulerError); darkColor = themeColorFromId(overviewRulerError); + zIndex = 30; break; } @@ -178,7 +190,8 @@ class ModelMarkerHandler { color, darkColor, position: OverviewRulerLane.Right - } + }, + zIndex }; } } @@ -192,6 +205,8 @@ interface IRawConfig { insertSpaces?: any; detectIndentation?: any; trimAutoWhitespace?: any; + largeFileSize?: any; + largeFileLineCount?: any; }; } @@ -270,13 +285,31 @@ export class ModelServiceImpl implements IModelService { detectIndentation = (config.editor.detectIndentation === 'false' ? false : Boolean(config.editor.detectIndentation)); } + let largeFileSize = EDITOR_MODEL_DEFAULTS.largeFileSize; + if (config.editor && typeof config.editor.largeFileSize !== 'undefined') { + let parsedlargeFileSize = parseInt(config.editor.largeFileSize, 10); + if (!isNaN(parsedlargeFileSize)) { + largeFileSize = parsedlargeFileSize; + } + } + + let largeFileLineCount = EDITOR_MODEL_DEFAULTS.largeFileLineCount; + if (config.editor && typeof config.editor.largeFileLineCount !== 'undefined') { + let parsedlargeFileLineCount = parseInt(config.editor.largeFileLineCount, 10); + if (!isNaN(parsedlargeFileLineCount)) { + largeFileLineCount = parsedlargeFileLineCount; + } + } + return { isForSimpleWidget: isForSimpleWidget, tabSize: tabSize, insertSpaces: insertSpaces, detectIndentation: detectIndentation, defaultEOL: newDefaultEOL, - trimAutoWhitespace: trimAutoWhitespace + trimAutoWhitespace: trimAutoWhitespace, + largeFileSize: largeFileSize, + largeFileLineCount: largeFileLineCount }; } @@ -396,12 +429,14 @@ export class ModelServiceImpl implements IModelService { } // Otherwise find a diff between the values and update model + model.pushStackElement(); model.setEOL(textBuffer.getEOL() === '\r\n' ? EndOfLineSequence.CRLF : EndOfLineSequence.LF); model.pushEditOperations( - [new Selection(1, 1, 1, 1)], + [], ModelServiceImpl._computeEdits(model, textBuffer), - (inverseEditOperations: IIdentifiedSingleEditOperation[]) => [new Selection(1, 1, 1, 1)] + (inverseEditOperations: IIdentifiedSingleEditOperation[]) => [] ); + model.pushStackElement(); } private static _commonPrefix(a: ILineSequence, aLen: number, aDelta: number, b: ILineSequence, bLen: number, bDelta: number): number { @@ -451,7 +486,7 @@ export class ModelServiceImpl implements IModelService { newRange = new Range(1, 1, textBufferLineCount, 1 + textBuffer.getLineLength(textBufferLineCount)); } - return [EditOperation.replace(oldRange, textBuffer.getValueInRange(newRange, EndOfLinePreference.TextDefined))]; + return [EditOperation.replaceMove(oldRange, textBuffer.getValueInRange(newRange, EndOfLinePreference.TextDefined))]; } public createModel(value: string | ITextBufferFactory, modeOrPromise: TPromise | IMode, resource: URI, isForSimpleWidget: boolean = false): ITextModel { diff --git a/src/vs/editor/common/services/resourceConfigurationImpl.ts b/src/vs/editor/common/services/resourceConfigurationImpl.ts index a407bf1c698..6b75152e474 100644 --- a/src/vs/editor/common/services/resourceConfigurationImpl.ts +++ b/src/vs/editor/common/services/resourceConfigurationImpl.ts @@ -11,6 +11,7 @@ import { ITextResourceConfigurationService } from 'vs/editor/common/services/res import { IPosition, Position } from 'vs/editor/common/core/position'; import { IModeService } from 'vs/editor/common/services/modeService'; import { IModelService } from 'vs/editor/common/services/modelService'; +import { basename } from 'vs/base/common/paths'; export class TextResourceConfigurationService extends Disposable implements ITextResourceConfigurationService { @@ -42,6 +43,6 @@ export class TextResourceConfigurationService extends Disposable implements ITex if (model) { return position ? this.modeService.getLanguageIdentifier(model.getLanguageIdAtPosition(position.lineNumber, position.column)).language : model.getLanguageIdentifier().language; } - return this.modeService.getModeIdByFilenameOrFirstLine(resource.fsPath); + return this.modeService.getModeIdByFilenameOrFirstLine(basename(resource.path)); } } \ No newline at end of file diff --git a/src/vs/editor/common/view/editorColorRegistry.ts b/src/vs/editor/common/view/editorColorRegistry.ts index e92db6d8195..92f67358ba7 100644 --- a/src/vs/editor/common/view/editorColorRegistry.ts +++ b/src/vs/editor/common/view/editorColorRegistry.ts @@ -20,8 +20,12 @@ export const editorCursorForeground = registerColor('editorCursor.foreground', { 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 editorActiveIndentGuides = registerColor('editorIndentGuide.activeBackground', { dark: editorWhitespaces, light: editorWhitespaces, hc: editorWhitespaces }, nls.localize('editorActiveIndentGuide', 'Color of the active editor indentation guides.')); export const editorLineNumbers = registerColor('editorLineNumber.foreground', { dark: '#5A5A5A', light: '#2B91AF', hc: Color.white }, nls.localize('editorLineNumbers', 'Color of editor line numbers.')); -export const editorActiveLineNumber = registerColor('editorActiveLineNumber.foreground', { dark: null, light: null, hc: null }, nls.localize('editorActiveLineNumber', 'Color of editor active line number')); + +const deprecatedEditorActiveLineNumber = registerColor('editorActiveLineNumber.foreground', { dark: null, light: null, hc: null }, nls.localize('editorActiveLineNumber', 'Color of editor active line number'), false, nls.localize('deprecatedEditorActiveLineNumber', 'Id is deprecated. Use \'editorLineNumber.activeForeground\' instead.')); +export const editorActiveLineNumber = registerColor('editorLineNumber.activeForeground', { dark: deprecatedEditorActiveLineNumber, light: deprecatedEditorActiveLineNumber, hc: deprecatedEditorActiveLineNumber }, nls.localize('editorActiveLineNumber', 'Color of editor active line number')); + export const editorRuler = registerColor('editorRuler.foreground', { dark: '#5A5A5A', light: Color.lightgrey, hc: Color.white }, nls.localize('editorRuler', 'Color of the editor rulers.')); export const editorCodeLensForeground = registerColor('editorCodeLens.foreground', { dark: '#999999', light: '#999999', hc: '#999999' }, nls.localize('editorCodeLensForeground', 'Foreground color of editor code lenses')); @@ -53,27 +57,32 @@ export const overviewRulerInfo = registerColor('editorOverviewRuler.infoForegrou // contains all color rules that used to defined in editor/browser/widget/editor.css registerThemingParticipant((theme, collector) => { - let background = theme.getColor(editorBackground); + const background = theme.getColor(editorBackground); if (background) { collector.addRule(`.monaco-editor, .monaco-editor-background, .monaco-editor .inputarea.ime-input { background-color: ${background}; }`); } - let foreground = theme.getColor(editorForeground); + + const foreground = theme.getColor(editorForeground); if (foreground) { collector.addRule(`.monaco-editor, .monaco-editor .inputarea.ime-input { color: ${foreground}; }`); } - let gutter = theme.getColor(editorGutter); + + const gutter = theme.getColor(editorGutter); if (gutter) { collector.addRule(`.monaco-editor .margin { background-color: ${gutter}; }`); } - let rangeHighlight = theme.getColor(editorRangeHighlight); + + const rangeHighlight = theme.getColor(editorRangeHighlight); if (rangeHighlight) { collector.addRule(`.monaco-editor .rangeHighlight { background-color: ${rangeHighlight}; }`); } - let rangeHighlightBorder = theme.getColor(editorRangeHighlightBorder); + + const rangeHighlightBorder = theme.getColor(editorRangeHighlightBorder); if (rangeHighlightBorder) { - collector.addRule(`.monaco-editor .rangeHighlight { border: 1px dotted ${rangeHighlightBorder}; }`); + collector.addRule(`.monaco-editor .rangeHighlight { border: 1px ${theme.type === 'hc' ? 'dotted' : 'solid'} ${rangeHighlightBorder}; }`); } - let invisibles = theme.getColor(editorWhitespaces); + + const invisibles = theme.getColor(editorWhitespaces); if (invisibles) { collector.addRule(`.vs-whitespace { color: ${invisibles} !important; }`); } diff --git a/src/vs/editor/common/viewLayout/lineDecorations.ts b/src/vs/editor/common/viewLayout/lineDecorations.ts index a791061e928..fe3397d94de 100644 --- a/src/vs/editor/common/viewLayout/lineDecorations.ts +++ b/src/vs/editor/common/viewLayout/lineDecorations.ts @@ -58,7 +58,7 @@ export class LineDecoration { continue; } - if (range.isEmpty() && d.type === InlineDecorationType.Regular) { + if (range.isEmpty() && (d.type === InlineDecorationType.Regular || d.type === InlineDecorationType.RegularAffectingLetterSpacing)) { // Ignore empty range decorations continue; } diff --git a/src/vs/editor/common/viewLayout/viewLayout.ts b/src/vs/editor/common/viewLayout/viewLayout.ts index d04c5657afb..a7e1f091386 100644 --- a/src/vs/editor/common/viewLayout/viewLayout.ts +++ b/src/vs/editor/common/viewLayout/viewLayout.ts @@ -163,7 +163,7 @@ export class ViewLayout extends Disposable implements IViewLayout { // ---- view state - public saveState(): editorCommon.IViewState { + public saveState(): { scrollTop: number; scrollTopWithoutViewZones: number; scrollLeft: number; } { const currentScrollPosition = this.scrollable.getFutureScrollPosition(); let scrollTop = currentScrollPosition.scrollTop; let firstLineNumberInViewport = this._linesLayout.getLineNumberAtOrAfterVerticalOffset(scrollTop); @@ -175,17 +175,6 @@ export class ViewLayout extends Disposable implements IViewLayout { }; } - public reduceRestoreState(state: editorCommon.IViewState): { scrollLeft: number; scrollTop: number; } { - let restoreScrollTop = state.scrollTop; - if (typeof state.scrollTopWithoutViewZones === 'number' && !this._linesLayout.hasWhitespace()) { - restoreScrollTop = state.scrollTopWithoutViewZones; - } - return { - scrollLeft: state.scrollLeft, - scrollTop: restoreScrollTop - }; - } - // ---- IVerticalLayoutProvider public addWhitespace(afterLineNumber: number, ordinal: number, height: number): number { diff --git a/src/vs/editor/common/viewLayout/viewLineRenderer.ts b/src/vs/editor/common/viewLayout/viewLineRenderer.ts index a3c3bef72de..e4c075e7c15 100644 --- a/src/vs/editor/common/viewLayout/viewLineRenderer.ts +++ b/src/vs/editor/common/viewLayout/viewLineRenderer.ts @@ -36,7 +36,8 @@ export class RenderLineInput { public readonly useMonospaceOptimizations: boolean; public readonly lineContent: string; - public readonly mightContainRTL: boolean; + public readonly isBasicASCII: boolean; + public readonly containsRTL: boolean; public readonly fauxIndentLength: number; public readonly lineTokens: IViewLineTokens; public readonly lineDecorations: LineDecoration[]; @@ -50,7 +51,8 @@ export class RenderLineInput { constructor( useMonospaceOptimizations: boolean, lineContent: string, - mightContainRTL: boolean, + isBasicASCII: boolean, + containsRTL: boolean, fauxIndentLength: number, lineTokens: IViewLineTokens, lineDecorations: LineDecoration[], @@ -63,7 +65,8 @@ export class RenderLineInput { ) { this.useMonospaceOptimizations = useMonospaceOptimizations; this.lineContent = lineContent; - this.mightContainRTL = mightContainRTL; + this.isBasicASCII = isBasicASCII; + this.containsRTL = containsRTL; this.fauxIndentLength = fauxIndentLength; this.lineTokens = lineTokens; this.lineDecorations = lineDecorations; @@ -85,7 +88,8 @@ export class RenderLineInput { return ( this.useMonospaceOptimizations === other.useMonospaceOptimizations && this.lineContent === other.lineContent - && this.mightContainRTL === other.mightContainRTL + && this.isBasicASCII === other.isBasicASCII + && this.containsRTL === other.containsRTL && this.fauxIndentLength === other.fauxIndentLength && this.tabSize === other.tabSize && this.spaceWidth === other.spaceWidth @@ -217,14 +221,20 @@ export class CharacterMapping { } } +export const enum ForeignElementType { + None = 0, + Before = 1, + After = 2 +} + export class RenderLineOutput { _renderLineOutputBrand: void; readonly characterMapping: CharacterMapping; readonly containsRTL: boolean; - readonly containsForeignElements: boolean; + readonly containsForeignElements: ForeignElementType; - constructor(characterMapping: CharacterMapping, containsRTL: boolean, containsForeignElements: boolean) { + constructor(characterMapping: CharacterMapping, containsRTL: boolean, containsForeignElements: ForeignElementType) { this.characterMapping = characterMapping; this.containsRTL = containsRTL; this.containsForeignElements = containsForeignElements; @@ -234,7 +244,7 @@ export class RenderLineOutput { export function renderViewLine(input: RenderLineInput, sb: IStringBuilder): RenderLineOutput { if (input.lineContent.length === 0) { - let containsForeignElements = false; + let containsForeignElements = ForeignElementType.None; // This is basically for IE's hit test to work let content: string = '\u00a0'; @@ -244,13 +254,17 @@ export function renderViewLine(input: RenderLineInput, sb: IStringBuilder): Rend let classNames: string[] = []; for (let i = 0, len = input.lineDecorations.length; i < len; i++) { const lineDecoration = input.lineDecorations[i]; - if (lineDecoration.type !== InlineDecorationType.Regular) { + if (lineDecoration.type === InlineDecorationType.Before) { classNames.push(input.lineDecorations[i].className); - containsForeignElements = true; + containsForeignElements |= ForeignElementType.Before; + } + if (lineDecoration.type === InlineDecorationType.After) { + classNames.push(input.lineDecorations[i].className); + containsForeignElements |= ForeignElementType.After; } } - if (containsForeignElements) { + if (containsForeignElements !== ForeignElementType.None) { content = ``; } } @@ -271,7 +285,7 @@ export class RenderLineOutput2 { public readonly characterMapping: CharacterMapping, public readonly html: string, public readonly containsRTL: boolean, - public readonly containsForeignElements: boolean + public readonly containsForeignElements: ForeignElementType ) { } } @@ -289,7 +303,7 @@ class ResolvedRenderLineInput { public readonly len: number, public readonly isOverflowing: boolean, public readonly parts: LinePart[], - public readonly containsForeignElements: boolean, + public readonly containsForeignElements: ForeignElementType, public readonly tabSize: number, public readonly containsRTL: boolean, public readonly spaceWidth: number, @@ -319,22 +333,22 @@ function resolveRenderLineInput(input: RenderLineInput): ResolvedRenderLineInput if (input.renderWhitespace === RenderWhitespace.All || input.renderWhitespace === RenderWhitespace.Boundary) { tokens = _applyRenderWhitespace(lineContent, len, tokens, input.fauxIndentLength, input.tabSize, useMonospaceOptimizations, input.renderWhitespace === RenderWhitespace.Boundary); } - let containsForeignElements = false; + let containsForeignElements = ForeignElementType.None; if (input.lineDecorations.length > 0) { for (let i = 0, len = input.lineDecorations.length; i < len; i++) { const lineDecoration = input.lineDecorations[i]; - if (lineDecoration.type !== InlineDecorationType.Regular) { - containsForeignElements = true; - break; + if (lineDecoration.type === InlineDecorationType.RegularAffectingLetterSpacing) { + // Pretend there are foreign elements... although not 100% accurate. + containsForeignElements |= ForeignElementType.Before; + } else if (lineDecoration.type === InlineDecorationType.Before) { + containsForeignElements |= ForeignElementType.Before; + } else if (lineDecoration.type === InlineDecorationType.After) { + containsForeignElements |= ForeignElementType.After; } } tokens = _applyInlineDecorations(lineContent, len, tokens, input.lineDecorations); } - let containsRTL = false; - if (input.mightContainRTL) { - containsRTL = strings.containsRTL(lineContent); - } - if (!containsRTL && !input.fontLigatures) { + if (input.isBasicASCII && !input.fontLigatures) { tokens = splitLargeTokens(lineContent, tokens); } @@ -346,7 +360,7 @@ function resolveRenderLineInput(input: RenderLineInput): ResolvedRenderLineInput tokens, containsForeignElements, input.tabSize, - containsRTL, + input.containsRTL, input.spaceWidth, input.renderWhitespace, input.renderControlCharacters @@ -406,11 +420,6 @@ function splitLargeTokens(lineContent: string, tokens: LinePart[]): LinePart[] { const piecesCount = Math.ceil(diff / Constants.LongToken); for (let j = 1; j < piecesCount; j++) { let pieceEndIndex = lastTokenEndIndex + (j * Constants.LongToken); - let lastCharInPiece = lineContent.charCodeAt(pieceEndIndex - 1); - if (strings.isHighSurrogate(lastCharInPiece)) { - // Don't cut in the middle of a surrogate pair - pieceEndIndex--; - } result[resultLen++] = new LinePart(pieceEndIndex, tokenType); } result[resultLen++] = new LinePart(tokenEndIndex, tokenType); diff --git a/src/vs/editor/common/viewModel/splitLinesCollection.ts b/src/vs/editor/common/viewModel/splitLinesCollection.ts index 3141915e9df..73f02436877 100644 --- a/src/vs/editor/common/viewModel/splitLinesCollection.ts +++ b/src/vs/editor/common/viewModel/splitLinesCollection.ts @@ -14,7 +14,7 @@ import { WrappingIndent } from 'vs/editor/common/config/editorOptions'; import { ModelDecorationOptions, ModelDecorationOverviewRulerOptions } from 'vs/editor/common/model/textModel'; import { ThemeColor, ITheme } from 'vs/platform/theme/common/themeService'; import { Color } from 'vs/base/common/color'; -import { IModelDecoration, ITextModel, IModelDeltaDecoration, EndOfLinePreference } from 'vs/editor/common/model'; +import { IModelDecoration, ITextModel, IModelDeltaDecoration, EndOfLinePreference, IActiveIndentGuideInfo } from 'vs/editor/common/model'; export class OutputPosition { _outputPositionBrand: void; @@ -41,6 +41,7 @@ export interface ILineMapperFactory { export interface ISimpleModel { getLineTokens(lineNumber: number): LineTokens; getLineContent(lineNumber: number): string; + getLineLength(lineNumber: number): number; getLineMinColumn(lineNumber: number): number; getLineMaxColumn(lineNumber: number): number; getValueInRange(range: IRange, eol?: EndOfLinePreference): string; @@ -52,6 +53,7 @@ export interface ISplitLine { getViewLineCount(): number; getViewLineContent(model: ISimpleModel, modelLineNumber: number, outputLineIndex: number): string; + getViewLineLength(model: ISimpleModel, modelLineNumber: number, outputLineIndex: number): number; getViewLineMinColumn(model: ISimpleModel, modelLineNumber: number, outputLineIndex: number): number; getViewLineMaxColumn(model: ISimpleModel, modelLineNumber: number, outputLineIndex: number): number; getViewLineData(model: ISimpleModel, modelLineNumber: number, outputLineIndex: number): ViewLineData; @@ -80,8 +82,10 @@ export interface IViewModelLinesCollection { getViewLineCount(): number; warmUpLookupCache(viewStartLineNumber: number, viewEndLineNumber: number): void; + getActiveIndentGuide(viewLineNumber: number): IActiveIndentGuideInfo; getViewLinesIndentGuides(viewStartLineNumber: number, viewEndLineNumber: number): number[]; getViewLineContent(viewLineNumber: number): string; + getViewLineLength(viewLineNumber: number): number; getViewLineMinColumn(viewLineNumber: number): number; getViewLineMaxColumn(viewLineNumber: number): number; getViewLineData(viewLineNumber: number): ViewLineData; @@ -501,6 +505,22 @@ export class SplitLinesCollection implements IViewModelLinesCollection { this.prefixSumComputer.warmUpCache(viewStartLineNumber - 1, viewEndLineNumber - 1); } + public getActiveIndentGuide(viewLineNumber: number): IActiveIndentGuideInfo { + this._ensureValidState(); + viewLineNumber = this._toValidViewLineNumber(viewLineNumber); + + const modelPosition = this.convertViewPositionToModelPosition(viewLineNumber, this.getViewLineMinColumn(viewLineNumber)); + const result = this.model.getActiveIndentGuide(modelPosition.lineNumber); + + const viewStartPosition = this.convertModelPositionToViewPosition(result.startLineNumber, 1); + const viewEndPosition = this.convertModelPositionToViewPosition(result.endLineNumber, 1); + return { + startLineNumber: viewStartPosition.lineNumber, + endLineNumber: viewEndPosition.lineNumber, + indent: result.indent + }; + } + public getViewLinesIndentGuides(viewStartLineNumber: number, viewEndLineNumber: number): number[] { this._ensureValidState(); viewStartLineNumber = this._toValidViewLineNumber(viewStartLineNumber); @@ -570,6 +590,16 @@ export class SplitLinesCollection implements IViewModelLinesCollection { return this.lines[lineIndex].getViewLineContent(this.model, lineIndex + 1, remainder); } + public getViewLineLength(viewLineNumber: number): number { + this._ensureValidState(); + viewLineNumber = this._toValidViewLineNumber(viewLineNumber); + let r = this.prefixSumComputer.getIndexOf(viewLineNumber - 1); + let lineIndex = r.index; + let remainder = r.remainder; + + return this.lines[lineIndex].getViewLineLength(this.model, lineIndex + 1, remainder); + } + public getViewLineMinColumn(viewLineNumber: number): number { this._ensureValidState(); viewLineNumber = this._toValidViewLineNumber(viewLineNumber); @@ -815,6 +845,10 @@ class VisibleIdentitySplitLine implements ISplitLine { return model.getLineContent(modelLineNumber); } + public getViewLineLength(model: ISimpleModel, modelLineNumber: number, outputLineIndex: number): number { + return model.getLineLength(modelLineNumber); + } + public getViewLineMinColumn(model: ISimpleModel, modelLineNumber: number, outputLineIndex: number): number { return model.getLineMinColumn(modelLineNumber); } @@ -880,6 +914,10 @@ class InvisibleIdentitySplitLine implements ISplitLine { throw new Error('Not supported'); } + public getViewLineLength(model: ISimpleModel, modelLineNumber: number, outputLineIndex: number): number { + throw new Error('Not supported'); + } + public getViewLineMinColumn(model: ISimpleModel, modelLineNumber: number, outputLineIndex: number): number { throw new Error('Not supported'); } @@ -973,6 +1011,21 @@ export class SplitLine implements ISplitLine { return r; } + public getViewLineLength(model: ISimpleModel, modelLineNumber: number, outputLineIndex: number): number { + if (!this._isVisible) { + throw new Error('Not supported'); + } + let startOffset = this.getInputStartOffsetOfOutputLineIndex(outputLineIndex); + let endOffset = this.getInputEndOffsetOfOutputLineIndex(model, modelLineNumber, outputLineIndex); + let r = endOffset - startOffset; + + if (outputLineIndex > 0) { + r = this.wrappedIndent.length + r; + } + + return r; + } + public getViewLineMinColumn(model: ITextModel, modelLineNumber: number, outputLineIndex: number): number { if (!this._isVisible) { throw new Error('Not supported'); @@ -1205,6 +1258,14 @@ export class IdentityLinesCollection implements IViewModelLinesCollection { public warmUpLookupCache(viewStartLineNumber: number, viewEndLineNumber: number): void { } + public getActiveIndentGuide(viewLineNumber: number): IActiveIndentGuideInfo { + return { + startLineNumber: viewLineNumber, + endLineNumber: viewLineNumber, + indent: 0 + }; + } + public getViewLinesIndentGuides(viewStartLineNumber: number, viewEndLineNumber: number): number[] { const viewLineCount = viewEndLineNumber - viewStartLineNumber + 1; let result = new Array(viewLineCount); @@ -1218,6 +1279,10 @@ export class IdentityLinesCollection implements IViewModelLinesCollection { return this.model.getLineContent(viewLineNumber); } + public getViewLineLength(viewLineNumber: number): number { + return this.model.getLineLength(viewLineNumber); + } + public getViewLineMinColumn(viewLineNumber: number): number { return this.model.getLineMinColumn(viewLineNumber); } diff --git a/src/vs/editor/common/viewModel/viewModel.ts b/src/vs/editor/common/viewModel/viewModel.ts index c3b771d2c31..c97d919393a 100644 --- a/src/vs/editor/common/viewModel/viewModel.ts +++ b/src/vs/editor/common/viewModel/viewModel.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import { INewScrollPosition, IViewState } from 'vs/editor/common/editorCommon'; -import { EndOfLinePreference, IModelDecorationOptions } from 'vs/editor/common/model'; +import { INewScrollPosition } from 'vs/editor/common/editorCommon'; +import { EndOfLinePreference, IModelDecorationOptions, IActiveIndentGuideInfo } from 'vs/editor/common/model'; import { IViewLineTokens } from 'vs/editor/common/core/lineTokens'; import { Position, IPosition } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; @@ -15,6 +15,7 @@ import { Scrollable, IScrollPosition } from 'vs/base/common/scrollable'; import { IPartialViewLinesViewportData } from 'vs/editor/common/viewLayout/viewLinesViewportData'; import { IEditorWhitespace } from 'vs/editor/common/viewLayout/whitespaceComputer'; import { ITheme } from 'vs/platform/theme/common/themeService'; +import * as strings from 'vs/base/common/strings'; export interface IViewWhitespaceViewportData { readonly id: number; @@ -63,9 +64,6 @@ export interface IViewLayout { getLinesViewportDataAtScrollTop(scrollTop: number): IPartialViewLinesViewportData; getWhitespaces(): IEditorWhitespace[]; - saveState(): IViewState; - reduceRestoreState(state: IViewState): { scrollLeft: number; scrollTop: number; }; - isAfterLines(verticalOffset: number): boolean; getLineNumberAtVerticalOffset(verticalOffset: number): number; getVerticalOffsetForLineNumber(lineNumber: number): number; @@ -122,6 +120,7 @@ export interface IViewModel { * Gives a hint that a lot of requests are about to come in for these line numbers. */ setViewport(startLineNumber: number, endLineNumber: number, centeredLineNumber: number): void; + setHasFocus(hasFocus: boolean): void; getDecorationsInViewport(visibleRange: Range): ViewModelDecoration[]; getViewLineRenderingData(visibleRange: Range, lineNumber: number): ViewLineRenderingData; @@ -132,6 +131,8 @@ export interface IViewModel { getTabSize(): number; getLineCount(): number; getLineContent(lineNumber: number): string; + getLineLength(lineNumber: number): number; + getActiveIndentGuide(lineNumber: number): IActiveIndentGuideInfo; getLinesIndentGuides(startLineNumber: number, endLineNumber: number): number[]; getLineMinColumn(lineNumber: number): number; getLineMaxColumn(lineNumber: number): number; @@ -210,13 +211,13 @@ export class ViewLineRenderingData { */ public readonly content: string; /** - * If set to false, it is guaranteed that `content` contains only LTR chars. + * Describes if `content` contains RTL characters. */ - public readonly mightContainRTL: boolean; + public readonly containsRTL: boolean; /** - * If set to false, it is guaranteed that `content` contains only basic ASCII chars. + * Describes if `content` contains non basic ASCII chars. */ - public readonly mightContainNonBasicASCII: boolean; + public readonly isBasicASCII: boolean; /** * The tokens at this view line. */ @@ -243,18 +244,35 @@ export class ViewLineRenderingData { this.minColumn = minColumn; this.maxColumn = maxColumn; this.content = content; - this.mightContainRTL = mightContainRTL; - this.mightContainNonBasicASCII = mightContainNonBasicASCII; + + this.isBasicASCII = ViewLineRenderingData.isBasicASCII(content, mightContainNonBasicASCII); + this.containsRTL = ViewLineRenderingData.containsRTL(content, this.isBasicASCII, mightContainRTL); + this.tokens = tokens; this.inlineDecorations = inlineDecorations; this.tabSize = tabSize; } + + public static isBasicASCII(lineContent: string, mightContainNonBasicASCII: boolean): boolean { + if (mightContainNonBasicASCII) { + return strings.isBasicASCII(lineContent); + } + return true; + } + + public static containsRTL(lineContent: string, isBasicASCII: boolean, mightContainRTL: boolean): boolean { + if (!isBasicASCII && mightContainRTL) { + return strings.containsRTL(lineContent); + } + return false; + } } export const enum InlineDecorationType { Regular = 0, Before = 1, - After = 2 + After = 2, + RegularAffectingLetterSpacing = 3 } export class InlineDecoration { diff --git a/src/vs/editor/common/viewModel/viewModelDecorations.ts b/src/vs/editor/common/viewModel/viewModelDecorations.ts index 7f69b5af0c2..7faf9dfb56f 100644 --- a/src/vs/editor/common/viewModel/viewModelDecorations.ts +++ b/src/vs/editor/common/viewModel/viewModelDecorations.ts @@ -124,7 +124,7 @@ export class ViewModelDecorations implements IDisposable { decorationsInViewport[decorationsInViewportLen++] = viewModelDecoration; if (decorationOptions.inlineClassName) { - let inlineDecoration = new InlineDecoration(viewRange, decorationOptions.inlineClassName, InlineDecorationType.Regular); + let inlineDecoration = new InlineDecoration(viewRange, decorationOptions.inlineClassName, decorationOptions.inlineClassNameAffectsLetterSpacing ? InlineDecorationType.RegularAffectingLetterSpacing : InlineDecorationType.Regular); let intersectedStartLineNumber = Math.max(startLineNumber, viewRange.startLineNumber); let intersectedEndLineNumber = Math.min(endLineNumber, viewRange.endLineNumber); for (let j = intersectedStartLineNumber; j <= intersectedEndLineNumber; j++) { diff --git a/src/vs/editor/common/viewModel/viewModelImpl.ts b/src/vs/editor/common/viewModel/viewModelImpl.ts index 6c8176bf41b..8fbfa9939e7 100644 --- a/src/vs/editor/common/viewModel/viewModelImpl.ts +++ b/src/vs/editor/common/viewModel/viewModelImpl.ts @@ -23,7 +23,7 @@ import { Color } from 'vs/base/common/color'; import { IDisposable } from 'vs/base/common/lifecycle'; import { ITheme } from 'vs/platform/theme/common/themeService'; import { ModelDecorationOverviewRulerOptions } from 'vs/editor/common/model/textModel'; -import { ITextModel, EndOfLinePreference } from 'vs/editor/common/model'; +import { ITextModel, EndOfLinePreference, TrackedRangeStickiness, IActiveIndentGuideInfo } from 'vs/editor/common/model'; const USE_IDENTITY_LINES_COLLECTION = true; @@ -32,20 +32,26 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel private readonly editorId: number; private readonly configuration: editorCommon.IConfiguration; private readonly model: ITextModel; + private hasFocus: boolean; + private viewportStartLine: number; + private viewportStartLineTrackedRange: string; + private viewportStartLineTop: number; private readonly lines: IViewModelLinesCollection; public readonly coordinatesConverter: ICoordinatesConverter; public readonly viewLayout: ViewLayout; private readonly decorations: ViewModelDecorations; - private _centeredViewLine: number; - constructor(editorId: number, configuration: editorCommon.IConfiguration, model: ITextModel, scheduleAtNextAnimationFrame: (callback: () => void) => IDisposable) { super(); this.editorId = editorId; this.configuration = configuration; this.model = model; + this.hasFocus = false; + this.viewportStartLine = -1; + this.viewportStartLineTrackedRange = null; + this.viewportStartLineTop = 0; if (USE_IDENTITY_LINES_COLLECTION && this.model.isTooLargeForTokenization()) { @@ -83,8 +89,6 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel } })); - this._centeredViewLine = -1; - this.decorations = new ViewModelDecorations(this.editorId, this.model, this.configuration, this.lines, this.coordinatesConverter); this._registerModelEvents(); @@ -114,13 +118,22 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel super.dispose(); this.decorations.dispose(); this.lines.dispose(); + this.viewportStartLineTrackedRange = this.model._setTrackedRange(this.viewportStartLineTrackedRange, null, TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges); + } + + public setHasFocus(hasFocus: boolean): void { + this.hasFocus = hasFocus; } private _onConfigurationChanged(eventsCollector: viewEvents.ViewEventsCollector, e: IConfigurationChangedEvent): void { // We might need to restore the current centered view range, so save it (if available) - const previousCenteredModelRange = this.getCenteredRangeInViewport(); - let revealPreviousCenteredModelRange = false; + let previousViewportStartModelPosition: Position = null; + if (this.viewportStartLine !== -1) { + let previousViewportStartViewPosition = new Position(this.viewportStartLine, this.getLineMinColumn(this.viewportStartLine)); + previousViewportStartModelPosition = this.coordinatesConverter.convertViewPositionToModelPosition(previousViewportStartViewPosition); + } + let restorePreviousViewportStart = false; const conf = this.configuration.editor; @@ -133,7 +146,7 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel if (this.viewLayout.getCurrentScrollTop() !== 0) { // Never change the scroll position from 0 to something else... - revealPreviousCenteredModelRange = true; + restorePreviousViewportStart = true; } } @@ -146,23 +159,16 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel eventsCollector.emit(new viewEvents.ViewConfigurationChangedEvent(e)); this.viewLayout.onConfigurationChanged(e); - if (revealPreviousCenteredModelRange && previousCenteredModelRange) { - // modelLine -> viewLine - const newCenteredViewRange = this.coordinatesConverter.convertModelRangeToViewRange(previousCenteredModelRange); - - // Send a reveal event to restore the centered content - eventsCollector.emit(new viewEvents.ViewRevealRangeRequestEvent( - newCenteredViewRange, - viewEvents.VerticalRevealType.Center, - false, - editorCommon.ScrollType.Immediate - )); + if (restorePreviousViewportStart && previousViewportStartModelPosition) { + const viewPosition = this.coordinatesConverter.convertModelPositionToViewPosition(previousViewportStartModelPosition); + const viewPositionTop = this.viewLayout.getVerticalOffsetForLineNumber(viewPosition.lineNumber); + this.viewLayout.deltaScrollNow(0, viewPositionTop - this.viewportStartLineTop); } } private _registerModelEvents(): void { - this._register(this.model.onDidChangeRawContent((e) => { + this._register(this.model.onDidChangeRawContentFast((e) => { try { const eventsCollector = this._beginEmit(); @@ -236,8 +242,18 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel } // Update the configuration and reset the centered view line - this._centeredViewLine = -1; + this.viewportStartLine = -1; this.configuration.setMaxLineNumber(this.model.getLineCount()); + + // Recover viewport + if (!this.hasFocus && this.model.getAttachedEditorCount() >= 2 && this.viewportStartLineTrackedRange) { + const modelRange = this.model._getTrackedRange(this.viewportStartLineTrackedRange); + if (modelRange) { + const viewPosition = this.coordinatesConverter.convertModelPositionToViewPosition(modelRange.getStartPosition()); + const viewPositionTop = this.viewLayout.getVerticalOffsetForLineNumber(viewPosition.lineNumber); + this.viewLayout.deltaScrollNow(0, viewPositionTop - this.viewportStartLineTop); + } + } })); this._register(this.model.onDidChangeTokens((e) => { @@ -311,16 +327,6 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel } } - public getCenteredRangeInViewport(): Range { - if (this._centeredViewLine === -1) { - // Never got rendered or not rendered since last content change event - return null; - } - let viewLineNumber = this._centeredViewLine; - let currentCenteredViewRange = new Range(viewLineNumber, this.getLineMinColumn(viewLineNumber), viewLineNumber, this.getLineMaxColumn(viewLineNumber)); - return this.coordinatesConverter.convertViewRangeToModelRange(currentCenteredViewRange); - } - public getVisibleRanges(): Range[] { const visibleViewRange = this.getCompletelyVisibleViewRange(); const visibleRange = this.coordinatesConverter.convertViewRangeToModelRange(visibleViewRange); @@ -388,6 +394,43 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel ); } + public saveState(): editorCommon.IViewState { + const compatViewState = this.viewLayout.saveState(); + + const scrollTop = compatViewState.scrollTop; + const firstViewLineNumber = this.viewLayout.getLineNumberAtVerticalOffset(scrollTop); + const firstPosition = this.coordinatesConverter.convertViewPositionToModelPosition(new Position(firstViewLineNumber, this.getLineMinColumn(firstViewLineNumber))); + const firstPositionDeltaTop = this.viewLayout.getVerticalOffsetForLineNumber(firstViewLineNumber) - scrollTop; + + return { + scrollLeft: compatViewState.scrollLeft, + firstPosition: firstPosition, + firstPositionDeltaTop: firstPositionDeltaTop + }; + } + + public reduceRestoreState(state: editorCommon.IViewState): { scrollLeft: number; scrollTop: number; } { + if (typeof state.firstPosition === 'undefined') { + // This is a view state serialized by an older version + return this._reduceRestoreStateCompatibility(state); + } + + const modelPosition = this.model.validatePosition(state.firstPosition); + const viewPosition = this.coordinatesConverter.convertModelPositionToViewPosition(modelPosition); + const scrollTop = this.viewLayout.getVerticalOffsetForLineNumber(viewPosition.lineNumber) - state.firstPositionDeltaTop; + return { + scrollLeft: state.scrollLeft, + scrollTop: scrollTop + }; + } + + private _reduceRestoreStateCompatibility(state: editorCommon.IViewState): { scrollLeft: number; scrollTop: number; } { + return { + scrollLeft: state.scrollLeft, + scrollTop: state.scrollTopWithoutViewZones + }; + } + public getTabSize(): number { return this.model.getOptions().tabSize; } @@ -400,8 +443,16 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel * Gives a hint that a lot of requests are about to come in for these line numbers. */ public setViewport(startLineNumber: number, endLineNumber: number, centeredLineNumber: number): void { - this._centeredViewLine = centeredLineNumber; this.lines.warmUpLookupCache(startLineNumber, endLineNumber); + + this.viewportStartLine = startLineNumber; + let position = this.coordinatesConverter.convertViewPositionToModelPosition(new Position(startLineNumber, this.getLineMinColumn(startLineNumber))); + this.viewportStartLineTrackedRange = this.model._setTrackedRange(this.viewportStartLineTrackedRange, new Range(position.lineNumber, position.column, position.lineNumber, position.column), TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges); + this.viewportStartLineTop = this.viewLayout.getVerticalOffsetForLineNumber(startLineNumber); + } + + public getActiveIndentGuide(lineNumber: number): IActiveIndentGuideInfo { + return this.lines.getActiveIndentGuide(lineNumber); } public getLinesIndentGuides(startLineNumber: number, endLineNumber: number): number[] { @@ -412,6 +463,10 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel return this.lines.getViewLineContent(lineNumber); } + public getLineLength(lineNumber: number): number { + return this.lines.getViewLineLength(lineNumber); + } + public getLineMinColumn(lineNumber: number): number { return this.lines.getViewLineMinColumn(lineNumber); } diff --git a/src/vs/editor/contrib/caretOperations/transpose.ts b/src/vs/editor/contrib/caretOperations/transpose.ts index 8c7bfd8dfdc..6523327b253 100644 --- a/src/vs/editor/contrib/caretOperations/transpose.ts +++ b/src/vs/editor/contrib/caretOperations/transpose.ts @@ -6,15 +6,56 @@ import * as nls from 'vs/nls'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { isLowSurrogate, isHighSurrogate } from 'vs/base/common/strings'; import { Range } from 'vs/editor/common/core/range'; +import { Position, IPosition } from 'vs/editor/common/core/position'; import { ICommand } from 'vs/editor/common/editorCommon'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { registerEditorAction, EditorAction, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { ReplaceCommand } from 'vs/editor/common/commands/replaceCommand'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { ITextModel } from 'vs/editor/common/model'; class TransposeLettersAction extends EditorAction { + private positionLeftOf(start: IPosition, model: ITextModel): Position { + let column = start.column; + let lineNumber = start.lineNumber; + + if (column > model.getLineMinColumn(lineNumber)) { + if (isLowSurrogate(model.getLineContent(lineNumber).charCodeAt(column - 2))) { + // character before column is a low surrogate + column = column - 2; + } else { + column = column - 1; + } + } else if (lineNumber > 1) { + lineNumber = lineNumber - 1; + column = model.getLineMaxColumn(lineNumber); + } + + return new Position(lineNumber, column); + } + + private positionRightOf(start: IPosition, model: ITextModel): Position { + let column = start.column; + let lineNumber = start.lineNumber; + + if (column < model.getLineMaxColumn(lineNumber)) { + if (isHighSurrogate(model.getLineContent(lineNumber).charCodeAt(column - 1))) { + // character after column is a high surrogate + column = column + 2; + } else { + column = column + 1; + } + } else if (lineNumber < model.getLineCount()) { + lineNumber = lineNumber + 1; + column = 0; + } + + return new Position(lineNumber, column); + } + constructor() { super({ id: 'editor.action.transposeLetters', @@ -36,30 +77,35 @@ class TransposeLettersAction extends EditorAction { let commands: ICommand[] = []; let selections = editor.getSelections(); - for (let i = 0; i < selections.length; i++) { - let selection = selections[i]; + for (let selection of selections) { if (!selection.isEmpty()) { continue; } + let lineNumber = selection.startLineNumber; let column = selection.startColumn; - if (column === 1) { - // at the beginning of line - continue; - } - let maxColumn = model.getLineMaxColumn(lineNumber); - if (column === maxColumn) { - // at the end of line + + let lastColumn = model.getLineMaxColumn(lineNumber); + + if (lineNumber === 1 && (column === 1 || (column === 2 && lastColumn === 2))) { + // at beginning of file, nothing to do continue; } - let lineContent = model.getLineContent(lineNumber); - let charToTheLeft = lineContent.charAt(column - 2); - let charToTheRight = lineContent.charAt(column - 1); + // handle special case: when at end of line, transpose left two chars + // otherwise, transpose left and right chars + let endPosition = (column === lastColumn) ? + selection.getPosition() : + this.positionRightOf(selection.getPosition(), model); - let replaceRange = new Range(lineNumber, column - 1, lineNumber, column + 1); + let middlePosition = this.positionLeftOf(endPosition, model); + let beginPosition = this.positionLeftOf(middlePosition, model); - commands.push(new ReplaceCommand(replaceRange, charToTheRight + charToTheLeft)); + let leftChar = model.getValueInRange(Range.fromPositions(beginPosition, middlePosition)); + let rightChar = model.getValueInRange(Range.fromPositions(middlePosition, endPosition)); + + let replaceRange = Range.fromPositions(beginPosition, endPosition); + commands.push(new ReplaceCommand(replaceRange, rightChar + leftChar)); } if (commands.length > 0) { diff --git a/src/vs/editor/contrib/quickFix/quickFix.ts b/src/vs/editor/contrib/codeAction/codeAction.ts similarity index 59% rename from src/vs/editor/contrib/quickFix/quickFix.ts rename to src/vs/editor/contrib/codeAction/codeAction.ts index 8e273057dc1..2a7e9fb9d30 100644 --- a/src/vs/editor/contrib/quickFix/quickFix.ts +++ b/src/vs/editor/contrib/codeAction/codeAction.ts @@ -2,46 +2,58 @@ * 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 URI from 'vs/base/common/uri'; -import { ITextModel } from 'vs/editor/common/model'; -import { Range } from 'vs/editor/common/core/range'; -import { CodeActionProviderRegistry, CodeAction } from 'vs/editor/common/modes'; +import { isFalsyOrEmpty, mergeSort, flatten } from 'vs/base/common/arrays'; import { asWinJsPromise } from 'vs/base/common/async'; +import { illegalArgument, onUnexpectedExternalError } from 'vs/base/common/errors'; +import URI from 'vs/base/common/uri'; 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 { registerLanguageCommand } from 'vs/editor/browser/editorExtensions'; -import { isFalsyOrEmpty, mergeSort } from 'vs/base/common/arrays'; -import { CodeActionKind } from './codeActionTrigger'; +import { Range } from 'vs/editor/common/core/range'; +import { ITextModel } from 'vs/editor/common/model'; +import { CodeAction, CodeActionProviderRegistry } from 'vs/editor/common/modes'; +import { IModelService } from 'vs/editor/common/services/modelService'; +import { CodeActionFilter, CodeActionKind } from './codeActionTrigger'; -export function getCodeActions(model: ITextModel, range: Range, scope?: CodeActionKind): TPromise { +export function getCodeActions(model: ITextModel, range: Range, filter?: CodeActionFilter): TPromise { + const codeActionContext = { only: filter && filter.kind ? filter.kind.value : undefined }; - const allResults: CodeAction[] = []; const promises = CodeActionProviderRegistry.all(model).map(support => { - return asWinJsPromise(token => support.provideCodeActions(model, range, { only: scope ? scope.value : undefined }, token)).then(result => { - if (Array.isArray(result)) { - for (const quickFix of result) { - if (quickFix) { - if (!scope || (quickFix.kind && scope.contains(quickFix.kind))) { - allResults.push(quickFix); - } - } - } + return asWinJsPromise(token => support.provideCodeActions(model, range, codeActionContext, token)).then(providedCodeActions => { + if (!Array.isArray(providedCodeActions)) { + return []; } - }, err => { + return providedCodeActions.filter(action => isValidAction(filter, action)); + }, (err): CodeAction[] => { onUnexpectedExternalError(err); + return []; }); }); - return TPromise.join(promises).then( - () => mergeSort(allResults, codeActionsComparator) - ); + return TPromise.join(promises) + .then(flatten) + .then(allCodeActions => mergeSort(allCodeActions, codeActionsComparator)); +} + +function isValidAction(filter: CodeActionFilter | undefined, action: CodeAction): boolean { + if (!action) { + return false; + } + + // Filter out actions by kind + if (filter && filter.kind && (!action.kind || !filter.kind.contains(action.kind))) { + return false; + } + + // Don't return source actions unless they are explicitly requested + if (action.kind && CodeActionKind.Source.contains(action.kind) && (!filter || !filter.includeSourceActions)) { + return false; + } + + return true; } function codeActionsComparator(a: CodeAction, b: CodeAction): number { - const aHasDiags = !isFalsyOrEmpty(a.diagnostics); const bHasDiags = !isFalsyOrEmpty(b.diagnostics); if (aHasDiags) { @@ -58,7 +70,6 @@ function codeActionsComparator(a: CodeAction, b: CodeAction): number { } registerLanguageCommand('_executeCodeActionProvider', function (accessor, args) { - const { resource, range } = args; if (!(resource instanceof URI) || !Range.isIRange(range)) { throw illegalArgument(); diff --git a/src/vs/editor/contrib/quickFix/quickFixCommands.ts b/src/vs/editor/contrib/codeAction/codeActionCommands.ts similarity index 56% rename from src/vs/editor/contrib/quickFix/quickFixCommands.ts rename to src/vs/editor/contrib/codeAction/codeActionCommands.ts index 8f499e12d57..3083b047c8d 100644 --- a/src/vs/editor/contrib/quickFix/quickFixCommands.ts +++ b/src/vs/editor/contrib/codeAction/codeActionCommands.ts @@ -2,30 +2,37 @@ * 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 nls from 'vs/nls'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; -import { ICommandService } from 'vs/platform/commands/common/commands'; -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 { optional } from 'vs/platform/instantiation/common/instantiation'; -import { IMarkerService } from 'vs/platform/markers/common/markers'; +import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { EditorAction, EditorCommand, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { BulkEdit } from 'vs/editor/browser/services/bulkEdit'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; -import { registerEditorAction, registerEditorContribution, ServicesAccessor, EditorAction, EditorCommand, registerEditorCommand } from 'vs/editor/browser/editorExtensions'; -import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { QuickFixContextMenu } from './quickFixWidget'; -import { LightBulbWidget } from './lightBulbWidget'; -import { QuickFixModel, QuickFixComputeEvent } from './quickFixModel'; -import { CodeActionKind, CodeActionAutoApply } from './codeActionTrigger'; -import { TPromise } from 'vs/base/common/winjs.base'; import { CodeAction } from 'vs/editor/common/modes'; -import { BulkEdit } from 'vs/editor/browser/services/bulkEdit'; -import { IFileService } from 'vs/platform/files/common/files'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; +import { MessageController } from 'vs/editor/contrib/message/messageController'; +import * as nls from 'vs/nls'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IFileService } from 'vs/platform/files/common/files'; +import { optional } from 'vs/platform/instantiation/common/instantiation'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { IMarkerService } from 'vs/platform/markers/common/markers'; +import { CodeActionModel, CodeActionsComputeEvent, SUPPORTED_CODE_ACTIONS } from './codeActionModel'; +import { CodeActionAutoApply, CodeActionFilter, CodeActionKind } from './codeActionTrigger'; +import { CodeActionContextMenu } from './codeActionWidget'; +import { LightBulbWidget } from './lightBulbWidget'; +import { escapeRegExpCharacters } from 'vs/base/common/strings'; + +function contextKeyForSupportedActions(kind: CodeActionKind) { + return ContextKeyExpr.regex( + SUPPORTED_CODE_ACTIONS.keys()[0], + new RegExp('(\\s|^)' + escapeRegExpCharacters(kind.value) + '\\b')); +} export class QuickFixController implements IEditorContribution { @@ -36,8 +43,8 @@ export class QuickFixController implements IEditorContribution { } private _editor: ICodeEditor; - private _model: QuickFixModel; - private _quickFixContextMenu: QuickFixContextMenu; + private _model: CodeActionModel; + private _codeActionContextMenu: CodeActionContextMenu; private _lightBulbWidget: LightBulbWidget; private _disposables: IDisposable[] = []; @@ -51,16 +58,16 @@ export class QuickFixController implements IEditorContribution { @optional(IFileService) private _fileService: IFileService ) { this._editor = editor; - this._model = new QuickFixModel(this._editor, markerService); - this._quickFixContextMenu = new QuickFixContextMenu(editor, contextMenuService, action => this._onApplyCodeAction(action)); + this._model = new CodeActionModel(this._editor, markerService, contextKeyService); + this._codeActionContextMenu = new CodeActionContextMenu(editor, contextMenuService, action => this._onApplyCodeAction(action)); this._lightBulbWidget = new LightBulbWidget(editor); this._updateLightBulbTitle(); this._disposables.push( - this._quickFixContextMenu.onDidExecuteCodeAction(_ => this._model.trigger({ type: 'auto' })), + this._codeActionContextMenu.onDidExecuteCodeAction(_ => this._model.trigger({ type: 'auto', filter: {} })), this._lightBulbWidget.onClick(this._handleLightBulbSelect, this), - this._model.onDidChangeFixes(e => this._onQuickFixEvent(e)), + this._model.onDidChangeFixes(e => this._onCodeActionsEvent(e)), this._keybindingService.onDidUpdateKeybindings(this._updateLightBulbTitle, this) ); } @@ -70,28 +77,28 @@ export class QuickFixController implements IEditorContribution { dispose(this._disposables); } - private _onQuickFixEvent(e: QuickFixComputeEvent): void { - if (e && e.trigger.kind) { + private _onCodeActionsEvent(e: CodeActionsComputeEvent): void { + if (e && e.trigger.filter && e.trigger.filter.kind) { // Triggered for specific scope // Apply if we only have one action or requested autoApply, otherwise show menu - e.fixes.then(fixes => { + e.actions.then(fixes => { if (e.trigger.autoApply === CodeActionAutoApply.First || (e.trigger.autoApply === CodeActionAutoApply.IfSingle && fixes.length === 1)) { this._onApplyCodeAction(fixes[0]); } else { - this._quickFixContextMenu.show(e.fixes, e.position); + this._codeActionContextMenu.show(e.actions, e.position); } }); return; } if (e && e.trigger.type === 'manual') { - this._quickFixContextMenu.show(e.fixes, e.position); - } else if (e && e.fixes) { + this._codeActionContextMenu.show(e.actions, e.position); + } else if (e && e.actions) { // auto magically triggered // * update an existing list of code actions // * manage light bulb - if (this._quickFixContextMenu.isVisible) { - this._quickFixContextMenu.show(e.fixes, e.position); + if (this._codeActionContextMenu.isVisible) { + this._codeActionContextMenu.show(e.actions, e.position); } else { this._lightBulbWidget.model = e; } @@ -105,15 +112,11 @@ export class QuickFixController implements IEditorContribution { } private _handleLightBulbSelect(coords: { x: number, y: number }): void { - this._quickFixContextMenu.show(this._lightBulbWidget.model.fixes, coords); + this._codeActionContextMenu.show(this._lightBulbWidget.model.actions, coords); } - public triggerFromEditorSelection(): void { - this._model.trigger({ type: 'manual' }); - } - - public triggerCodeActionFromEditorSelection(kind?: CodeActionKind, autoApply?: CodeActionAutoApply): void { - this._model.trigger({ type: 'manual', kind, autoApply }); + public triggerFromEditorSelection(filter?: CodeActionFilter, autoApply?: CodeActionAutoApply): TPromise { + return this._model.trigger({ type: 'manual', filter, autoApply }); } private _updateLightBulbTitle(): void { @@ -138,6 +141,25 @@ export class QuickFixController implements IEditorContribution { } } +function showCodeActionsForEditorSelection( + editor: ICodeEditor, + notAvailableMessage: string, + filter?: CodeActionFilter, + autoApply?: CodeActionAutoApply +) { + const controller = QuickFixController.get(editor); + if (!controller) { + return; + } + + const pos = editor.getPosition(); + controller.triggerFromEditorSelection(filter, autoApply).then(codeActions => { + if (!codeActions || !codeActions.length) { + MessageController.get(editor).showMessage(notAvailableMessage, pos); + } + }); +} + export class QuickFixAction extends EditorAction { static readonly Id = 'editor.action.quickFix'; @@ -145,7 +167,7 @@ export class QuickFixAction extends EditorAction { constructor() { super({ id: QuickFixAction.Id, - label: nls.localize('quickfix.trigger.label', "Quick Fix"), + label: nls.localize('quickfix.trigger.label', "Quick Fix..."), alias: 'Quick Fix', precondition: ContextKeyExpr.and(EditorContextKeys.writable, EditorContextKeys.hasCodeActionsProvider), kbOpts: { @@ -156,10 +178,7 @@ export class QuickFixAction extends EditorAction { } public run(accessor: ServicesAccessor, editor: ICodeEditor): void { - let controller = QuickFixController.get(editor); - if (controller) { - controller.triggerFromEditorSelection(); - } + return showCodeActionsForEditorSelection(editor, nls.localize('editor.action.quickFix.noneMessage', "No code actions available")); } } @@ -212,11 +231,8 @@ export class CodeActionCommand extends EditorCommand { } public runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, userArg: any) { - const controller = QuickFixController.get(editor); - if (controller) { - const args = CodeActionCommandArgs.fromUser(userArg); - controller.triggerCodeActionFromEditorSelection(args.kind, args.apply); - } + const args = CodeActionCommandArgs.fromUser(userArg); + return showCodeActionsForEditorSelection(editor, nls.localize('editor.action.quickFix.noneMessage', "No code actions available"), { kind: args.kind, includeSourceActions: true }, args.apply); } } @@ -228,26 +244,83 @@ export class RefactorAction extends EditorAction { constructor() { super({ id: RefactorAction.Id, - label: nls.localize('refactor.label', "Refactor"), + label: nls.localize('refactor.label', "Refactor..."), alias: 'Refactor', precondition: ContextKeyExpr.and(EditorContextKeys.writable, EditorContextKeys.hasCodeActionsProvider), kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, primary: KeyMod.WinCtrl | KeyMod.Shift | KeyCode.KEY_R + }, + menuOpts: { + group: '1_modification', + order: 2, + when: ContextKeyExpr.and( + EditorContextKeys.writable, + contextKeyForSupportedActions(CodeActionKind.Refactor)), } }); } public run(accessor: ServicesAccessor, editor: ICodeEditor): void { - const controller = QuickFixController.get(editor); - if (controller) { - controller.triggerCodeActionFromEditorSelection(CodeActionKind.Refactor, CodeActionAutoApply.Never); - } + return showCodeActionsForEditorSelection(editor, + nls.localize('editor.action.refactor.noneMessage', "No refactorings available"), + { kind: CodeActionKind.Refactor }, + CodeActionAutoApply.Never); } } -registerEditorContribution(QuickFixController); -registerEditorAction(QuickFixAction); -registerEditorAction(RefactorAction); -registerEditorCommand(new CodeActionCommand()); +export class SourceAction extends EditorAction { + + static readonly Id = 'editor.action.sourceAction'; + + constructor() { + super({ + id: SourceAction.Id, + label: nls.localize('source.label', "Source Action..."), + alias: 'Source Action', + precondition: ContextKeyExpr.and(EditorContextKeys.writable, EditorContextKeys.hasCodeActionsProvider), + menuOpts: { + group: '1_modification', + order: 2.1, + when: ContextKeyExpr.and( + EditorContextKeys.writable, + contextKeyForSupportedActions(CodeActionKind.Source)), + } + }); + } + + public run(accessor: ServicesAccessor, editor: ICodeEditor): void { + return showCodeActionsForEditorSelection(editor, + nls.localize('editor.action.source.noneMessage', "No source actions available"), + { kind: CodeActionKind.Source, includeSourceActions: true }, + CodeActionAutoApply.Never); + } +} + +export class OrganizeImportsAction extends EditorAction { + + static readonly Id = 'editor.action.organizeImports'; + + constructor() { + super({ + id: OrganizeImportsAction.Id, + label: nls.localize('organizeImports.label', "Organize Imports"), + alias: 'Organize Imports', + precondition: ContextKeyExpr.and( + EditorContextKeys.writable, + contextKeyForSupportedActions(CodeActionKind.SourceOrganizeImports)), + kbOpts: { + kbExpr: EditorContextKeys.editorTextFocus, + primary: KeyMod.Shift | KeyMod.Alt | KeyCode.KEY_O + } + }); + } + + public run(accessor: ServicesAccessor, editor: ICodeEditor): void { + return showCodeActionsForEditorSelection(editor, + nls.localize('editor.action.organize.noneMessage', "No organize imports action available"), + { kind: CodeActionKind.SourceOrganizeImports, includeSourceActions: true }, + CodeActionAutoApply.IfSingle); + } +} \ No newline at end of file diff --git a/src/vs/editor/contrib/codeAction/codeActionContributions.ts b/src/vs/editor/contrib/codeAction/codeActionContributions.ts new file mode 100644 index 00000000000..8653b935470 --- /dev/null +++ b/src/vs/editor/contrib/codeAction/codeActionContributions.ts @@ -0,0 +1,15 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { registerEditorAction, registerEditorCommand, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; +import { SourceAction, QuickFixController, QuickFixAction, CodeActionCommand, RefactorAction, OrganizeImportsAction } from 'vs/editor/contrib/codeAction/codeActionCommands'; + + +registerEditorContribution(QuickFixController); +registerEditorAction(QuickFixAction); +registerEditorAction(RefactorAction); +registerEditorAction(SourceAction); +registerEditorAction(OrganizeImportsAction); +registerEditorCommand(new CodeActionCommand()); diff --git a/src/vs/editor/contrib/quickFix/quickFixModel.ts b/src/vs/editor/contrib/codeAction/codeActionModel.ts similarity index 66% rename from src/vs/editor/contrib/quickFix/quickFixModel.ts rename to src/vs/editor/contrib/codeAction/codeActionModel.ts index 899b1d93da5..a369f31e28e 100644 --- a/src/vs/editor/contrib/quickFix/quickFixModel.ts +++ b/src/vs/editor/contrib/codeAction/codeActionModel.ts @@ -2,29 +2,31 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; -import { Event, Emitter, debounceEvent } from 'vs/base/common/event'; +import { Emitter, Event, 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 { IMarkerService } from 'vs/platform/markers/common/markers'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; -import { CodeActionProviderRegistry, CodeAction } from 'vs/editor/common/modes'; -import { getCodeActions } from './quickFix'; +import { CodeAction, CodeActionProviderRegistry } from 'vs/editor/common/modes'; +import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { IMarkerService } from 'vs/platform/markers/common/markers'; +import { getCodeActions } from './codeAction'; import { CodeActionTrigger } from './codeActionTrigger'; -import { Position } from 'vs/editor/common/core/position'; -import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -export class QuickFixOracle { +export const SUPPORTED_CODE_ACTIONS = new RawContextKey('supportedCodeAction', ''); + +export class CodeActionOracle { private _disposables: IDisposable[] = []; constructor( private _editor: ICodeEditor, private _markerService: IMarkerService, - private _signalChange: (e: QuickFixComputeEvent) => any, + private _signalChange: (e: CodeActionsComputeEvent) => any, delay: number = 250 ) { this._disposables.push( @@ -37,12 +39,12 @@ export class QuickFixOracle { this._disposables = dispose(this._disposables); } - trigger(trigger: CodeActionTrigger): void { + trigger(trigger: CodeActionTrigger) { let rangeOrSelection = this._getRangeOfMarker() || this._getRangeOfSelectionUnlessWhitespaceEnclosed(); if (!rangeOrSelection && trigger.type === 'manual') { rangeOrSelection = this._editor.getSelection(); } - this._createEventAndSignalChange(trigger, rangeOrSelection); + return this._createEventAndSignalChange(trigger, rangeOrSelection); } private _onMarkerChanges(resources: URI[]): void { @@ -99,51 +101,56 @@ export class QuickFixOracle { return selection; } - private _createEventAndSignalChange(trigger: CodeActionTrigger, rangeOrSelection: Range | Selection): void { + private _createEventAndSignalChange(trigger: CodeActionTrigger, rangeOrSelection: Range | Selection): TPromise { if (!rangeOrSelection) { // cancel this._signalChange({ trigger, range: undefined, position: undefined, - fixes: undefined, + actions: undefined, }); + return TPromise.as(undefined); } else { // actual const model = this._editor.getModel(); const range = model.validateRange(rangeOrSelection); const position = rangeOrSelection instanceof Selection ? rangeOrSelection.getPosition() : rangeOrSelection.getStartPosition(); - const fixes = getCodeActions(model, range, trigger && trigger.kind); + const actions = getCodeActions(model, range, trigger && trigger.filter); this._signalChange({ trigger, range, position, - fixes + actions }); + return actions; } } } -export interface QuickFixComputeEvent { +export interface CodeActionsComputeEvent { trigger: CodeActionTrigger; range: Range; position: Position; - fixes: TPromise; + actions: TPromise; } -export class QuickFixModel { +export class CodeActionModel { private _editor: ICodeEditor; private _markerService: IMarkerService; - private _quickFixOracle: QuickFixOracle; - private _onDidChangeFixes = new Emitter(); + private _codeActionOracle: CodeActionOracle; + private _onDidChangeFixes = new Emitter(); private _disposables: IDisposable[] = []; + private readonly _supportedCodeActions: IContextKey; - constructor(editor: ICodeEditor, markerService: IMarkerService) { + constructor(editor: ICodeEditor, markerService: IMarkerService, contextKeyService: IContextKeyService) { this._editor = editor; this._markerService = markerService; + this._supportedCodeActions = SUPPORTED_CODE_ACTIONS.bindTo(contextKeyService); + this._disposables.push(this._editor.onDidChangeModel(() => this._update())); this._disposables.push(this._editor.onDidChangeModelLanguage(() => this._update())); this._disposables.push(CodeActionProviderRegistry.onDidChange(this._update, this)); @@ -153,18 +160,18 @@ export class QuickFixModel { dispose(): void { this._disposables = dispose(this._disposables); - dispose(this._quickFixOracle); + dispose(this._codeActionOracle); } - get onDidChangeFixes(): Event { + get onDidChangeFixes(): Event { return this._onDidChangeFixes.event; } private _update(): void { - if (this._quickFixOracle) { - this._quickFixOracle.dispose(); - this._quickFixOracle = undefined; + if (this._codeActionOracle) { + this._codeActionOracle.dispose(); + this._codeActionOracle = undefined; this._onDidChangeFixes.fire(undefined); } @@ -172,14 +179,26 @@ export class QuickFixModel { && CodeActionProviderRegistry.has(this._editor.getModel()) && !this._editor.getConfiguration().readOnly) { - this._quickFixOracle = new QuickFixOracle(this._editor, this._markerService, p => this._onDidChangeFixes.fire(p)); - this._quickFixOracle.trigger({ type: 'auto' }); + const supportedActions: string[] = []; + for (const provider of CodeActionProviderRegistry.all(this._editor.getModel())) { + if (Array.isArray(provider.providedCodeActionKinds)) { + supportedActions.push(...provider.providedCodeActionKinds); + } + } + + this._supportedCodeActions.set(supportedActions.join(' ')); + + this._codeActionOracle = new CodeActionOracle(this._editor, this._markerService, p => this._onDidChangeFixes.fire(p)); + this._codeActionOracle.trigger({ type: 'auto' }); + } else { + this._supportedCodeActions.reset(); } } - trigger(trigger: CodeActionTrigger): void { - if (this._quickFixOracle) { - this._quickFixOracle.trigger(trigger); + trigger(trigger: CodeActionTrigger): TPromise { + if (this._codeActionOracle) { + return this._codeActionOracle.trigger(trigger); } + return TPromise.as(undefined); } } diff --git a/src/vs/editor/contrib/quickFix/codeActionTrigger.ts b/src/vs/editor/contrib/codeAction/codeActionTrigger.ts similarity index 69% rename from src/vs/editor/contrib/quickFix/codeActionTrigger.ts rename to src/vs/editor/contrib/codeAction/codeActionTrigger.ts index a210e484446..4f3c53b5dc9 100644 --- a/src/vs/editor/contrib/quickFix/codeActionTrigger.ts +++ b/src/vs/editor/contrib/codeAction/codeActionTrigger.ts @@ -10,6 +10,8 @@ export class CodeActionKind { public static readonly Empty = new CodeActionKind(''); public static readonly Refactor = new CodeActionKind('refactor'); + public static readonly Source = new CodeActionKind('source'); + public static readonly SourceOrganizeImports = new CodeActionKind('source.organizeImports'); constructor( public readonly value: string @@ -26,8 +28,13 @@ export enum CodeActionAutoApply { Never = 3 } +export interface CodeActionFilter { + readonly kind?: CodeActionKind; + readonly includeSourceActions?: boolean; +} + export interface CodeActionTrigger { - type: 'auto' | 'manual'; - kind?: CodeActionKind; - autoApply?: CodeActionAutoApply; + readonly type: 'auto' | 'manual'; + readonly filter?: CodeActionFilter; + readonly autoApply?: CodeActionAutoApply; } \ No newline at end of file diff --git a/src/vs/editor/contrib/quickFix/quickFixWidget.ts b/src/vs/editor/contrib/codeAction/codeActionWidget.ts similarity index 88% rename from src/vs/editor/contrib/quickFix/quickFixWidget.ts rename to src/vs/editor/contrib/codeAction/codeActionWidget.ts index 1fd65fa824b..d65b389d1ca 100644 --- a/src/vs/editor/contrib/quickFix/quickFixWidget.ts +++ b/src/vs/editor/contrib/codeAction/codeActionWidget.ts @@ -3,20 +3,19 @@ * 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 { always } from 'vs/base/common/async'; import { getDomNodePagePosition } from 'vs/base/browser/dom'; -import { Position } from 'vs/editor/common/core/position'; +import { Action } from 'vs/base/common/actions'; +import { always } from 'vs/base/common/async'; +import { canceled } from 'vs/base/common/errors'; +import { Emitter, Event } from 'vs/base/common/event'; +import { TPromise } from 'vs/base/common/winjs.base'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { Position } from 'vs/editor/common/core/position'; +import { ScrollType } from 'vs/editor/common/editorCommon'; import { CodeAction } from 'vs/editor/common/modes'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { Action } from 'vs/base/common/actions'; -import { Event, Emitter } from 'vs/base/common/event'; -import { ScrollType } from 'vs/editor/common/editorCommon'; -export class QuickFixContextMenu { +export class CodeActionContextMenu { private _visible: boolean; private _onDidExecuteCodeAction = new Emitter(); @@ -39,6 +38,12 @@ export class QuickFixContextMenu { () => this._onDidExecuteCodeAction.fire(undefined)); }); }); + }).then(actions => { + if (!this._editor.getDomNode()) { + // cancel when editor went off-dom + return TPromise.wrapError(canceled()); + } + return actions; }); this._contextMenuService.showContextMenu({ diff --git a/src/vs/editor/contrib/quickFix/lightBulbWidget.css b/src/vs/editor/contrib/codeAction/lightBulbWidget.css similarity index 100% rename from src/vs/editor/contrib/quickFix/lightBulbWidget.css rename to src/vs/editor/contrib/codeAction/lightBulbWidget.css diff --git a/src/vs/editor/contrib/quickFix/lightBulbWidget.ts b/src/vs/editor/contrib/codeAction/lightBulbWidget.ts similarity index 91% rename from src/vs/editor/contrib/quickFix/lightBulbWidget.ts rename to src/vs/editor/contrib/codeAction/lightBulbWidget.ts index 92bd859cdf9..07578f4a121 100644 --- a/src/vs/editor/contrib/quickFix/lightBulbWidget.ts +++ b/src/vs/editor/contrib/codeAction/lightBulbWidget.ts @@ -2,17 +2,16 @@ * 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!./lightBulbWidget'; -import { CancellationTokenSource } from 'vs/base/common/cancellation'; -import { dispose, IDisposable } from 'vs/base/common/lifecycle'; -import { Event, Emitter } from 'vs/base/common/event'; -import { GlobalMouseMoveMonitor, IStandardMouseMoveEventData, standardMouseMoveMerger } from 'vs/base/browser/globalMouseMoveMonitor'; import * as dom from 'vs/base/browser/dom'; -import { ICodeEditor, IContentWidget, IContentWidgetPosition, ContentWidgetPositionPreference } from 'vs/editor/browser/editorBrowser'; -import { QuickFixComputeEvent } from './quickFixModel'; +import { GlobalMouseMoveMonitor, IStandardMouseMoveEventData, standardMouseMoveMerger } from 'vs/base/browser/globalMouseMoveMonitor'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { Emitter, Event } from 'vs/base/common/event'; +import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import 'vs/css!./lightBulbWidget'; +import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from 'vs/editor/browser/editorBrowser'; import { TextModel } from 'vs/editor/common/model/textModel'; +import { CodeActionsComputeEvent } from './codeActionModel'; export class LightBulbWidget implements IDisposable, IContentWidget { @@ -26,7 +25,7 @@ export class LightBulbWidget implements IDisposable, IContentWidget { readonly onClick: Event<{ x: number, y: number }> = this._onClick.event; private _position: IContentWidgetPosition; - private _model: QuickFixComputeEvent; + private _model: CodeActionsComputeEvent; private _futureFixes = new CancellationTokenSource(); constructor(editor: ICodeEditor) { @@ -100,7 +99,7 @@ export class LightBulbWidget implements IDisposable, IContentWidget { return this._position; } - set model(value: QuickFixComputeEvent) { + set model(value: CodeActionsComputeEvent) { if (this._position && (!value.position || this._position.position.lineNumber !== value.position.lineNumber)) { // hide when getting a 'hide'-request or when currently @@ -115,7 +114,7 @@ export class LightBulbWidget implements IDisposable, IContentWidget { const { token } = this._futureFixes; this._model = value; - this._model.fixes.done(fixes => { + this._model.actions.done(fixes => { if (!token.isCancellationRequested && fixes && fixes.length > 0) { this._show(); } else { @@ -126,7 +125,7 @@ export class LightBulbWidget implements IDisposable, IContentWidget { }); } - get model(): QuickFixComputeEvent { + get model(): CodeActionsComputeEvent { return this._model; } diff --git a/src/vs/editor/contrib/quickFix/lightbulb-dark.svg b/src/vs/editor/contrib/codeAction/lightbulb-dark.svg similarity index 100% rename from src/vs/editor/contrib/quickFix/lightbulb-dark.svg rename to src/vs/editor/contrib/codeAction/lightbulb-dark.svg diff --git a/src/vs/editor/contrib/quickFix/lightbulb.svg b/src/vs/editor/contrib/codeAction/lightbulb.svg similarity index 100% rename from src/vs/editor/contrib/quickFix/lightbulb.svg rename to src/vs/editor/contrib/codeAction/lightbulb.svg diff --git a/src/vs/editor/contrib/quickFix/test/quickFix.test.ts b/src/vs/editor/contrib/codeAction/test/codeAction.test.ts similarity index 80% rename from src/vs/editor/contrib/quickFix/test/quickFix.test.ts rename to src/vs/editor/contrib/codeAction/test/codeAction.test.ts index 9693fce61de..e4b9936405e 100644 --- a/src/vs/editor/contrib/quickFix/test/quickFix.test.ts +++ b/src/vs/editor/contrib/codeAction/test/codeAction.test.ts @@ -10,11 +10,11 @@ import { TextModel } from 'vs/editor/common/model/textModel'; import { CodeActionProviderRegistry, LanguageIdentifier, CodeActionProvider, Command, WorkspaceEdit, ResourceTextEdit, CodeAction, CodeActionContext } from 'vs/editor/common/modes'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { Range } from 'vs/editor/common/core/range'; -import { getCodeActions } from 'vs/editor/contrib/quickFix/quickFix'; -import { CodeActionKind } from 'vs/editor/contrib/quickFix/codeActionTrigger'; +import { getCodeActions } from 'vs/editor/contrib/codeAction/codeAction'; +import { CodeActionKind } from 'vs/editor/contrib/codeAction/codeActionTrigger'; import { MarkerSeverity } from 'vs/platform/markers/common/markers'; -suite('QuickFix', () => { +suite('CodeAction', () => { let langId = new LanguageIdentifier('fooLang', 17); let uri = URI.parse('untitled:path'); @@ -136,20 +136,20 @@ suite('QuickFix', () => { disposables.push(CodeActionProviderRegistry.register('fooLang', provider)); { - const actions = await getCodeActions(model, new Range(1, 1, 2, 1), new CodeActionKind('a')); + const actions = await getCodeActions(model, new Range(1, 1, 2, 1), { kind: new CodeActionKind('a') }); assert.equal(actions.length, 2); assert.strictEqual(actions[0].title, 'a'); assert.strictEqual(actions[1].title, 'a.b'); } { - const actions = await getCodeActions(model, new Range(1, 1, 2, 1), new CodeActionKind('a.b')); + const actions = await getCodeActions(model, new Range(1, 1, 2, 1), { kind: new CodeActionKind('a.b') }); assert.equal(actions.length, 1); assert.strictEqual(actions[0].title, 'a.b'); } { - const actions = await getCodeActions(model, new Range(1, 1, 2, 1), new CodeActionKind('a.b.c')); + const actions = await getCodeActions(model, new Range(1, 1, 2, 1), { kind: new CodeActionKind('a.b.c') }); assert.equal(actions.length, 0); } }); @@ -165,8 +165,33 @@ suite('QuickFix', () => { disposables.push(CodeActionProviderRegistry.register('fooLang', provider)); - const actions = await getCodeActions(model, new Range(1, 1, 2, 1), new CodeActionKind('a')); + const actions = await getCodeActions(model, new Range(1, 1, 2, 1), { kind: new CodeActionKind('a') }); assert.equal(actions.length, 1); assert.strictEqual(actions[0].title, 'a'); }); + + test('getCodeActions should not return source code action by default', async function () { + const provider = new class implements CodeActionProvider { + provideCodeActions(): CodeAction[] { + return [ + { title: 'a', kind: CodeActionKind.Source.value }, + { title: 'b', kind: 'b' } + ]; + } + }; + + disposables.push(CodeActionProviderRegistry.register('fooLang', provider)); + + { + const actions = await getCodeActions(model, new Range(1, 1, 2, 1), {}); + assert.equal(actions.length, 1); + assert.strictEqual(actions[0].title, 'b'); + } + + { + const actions = await getCodeActions(model, new Range(1, 1, 2, 1), { kind: CodeActionKind.Source, includeSourceActions: true }); + assert.equal(actions.length, 1); + assert.strictEqual(actions[0].title, 'a'); + } + }); }); diff --git a/src/vs/editor/contrib/quickFix/test/quickFixModel.test.ts b/src/vs/editor/contrib/codeAction/test/codeActionModel.test.ts similarity index 91% rename from src/vs/editor/contrib/quickFix/test/quickFixModel.test.ts rename to src/vs/editor/contrib/codeAction/test/codeActionModel.test.ts index a6c3b4663a7..3b41bb9f7b3 100644 --- a/src/vs/editor/contrib/quickFix/test/quickFixModel.test.ts +++ b/src/vs/editor/contrib/codeAction/test/codeActionModel.test.ts @@ -10,13 +10,13 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { TextModel } from 'vs/editor/common/model/textModel'; import { createTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; import { MarkerService } from 'vs/platform/markers/common/markerService'; -import { QuickFixOracle } from 'vs/editor/contrib/quickFix/quickFixModel'; +import { CodeActionOracle } from 'vs/editor/contrib/codeAction/codeActionModel'; import { CodeActionProviderRegistry, LanguageIdentifier } from 'vs/editor/common/modes'; import { IDisposable } from 'vs/base/common/lifecycle'; import { Range } from 'vs/editor/common/core/range'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -suite('QuickFix', () => { +suite('CodeAction', () => { const languageIdentifier = new LanguageIdentifier('foo-lang', 3); let uri = URI.parse('untitled:path'); @@ -46,11 +46,11 @@ suite('QuickFix', () => { test('Orcale -> marker added', done => { - const oracle = new QuickFixOracle(editor, markerService, e => { + const oracle = new CodeActionOracle(editor, markerService, e => { assert.equal(e.trigger.type, 'auto'); - assert.ok(e.fixes); + assert.ok(e.actions); - e.fixes.then(fixes => { + e.actions.then(fixes => { oracle.dispose(); assert.equal(fixes.length, 1); done(); @@ -82,10 +82,10 @@ suite('QuickFix', () => { return new Promise((resolve, reject) => { - const oracle = new QuickFixOracle(editor, markerService, e => { + const oracle = new CodeActionOracle(editor, markerService, e => { assert.equal(e.trigger.type, 'auto'); - assert.ok(e.fixes); - e.fixes.then(fixes => { + assert.ok(e.actions); + e.actions.then(fixes => { oracle.dispose(); assert.equal(fixes.length, 1); resolve(undefined); @@ -115,8 +115,8 @@ suite('QuickFix', () => { }]); let fixes: TPromise[] = []; - let oracle = new QuickFixOracle(editor, markerService, e => { - fixes.push(e.fixes); + let oracle = new CodeActionOracle(editor, markerService, e => { + fixes.push(e.actions); }, 10); editor.setSelection({ startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 13 }); @@ -159,7 +159,7 @@ suite('QuickFix', () => { // case 1 - drag selection over multiple lines -> range of enclosed marker, position or marker await new Promise(resolve => { - let oracle = new QuickFixOracle(editor, markerService, e => { + let oracle = new CodeActionOracle(editor, markerService, e => { assert.equal(e.trigger.type, 'auto'); assert.deepEqual(e.range, { startLineNumber: 3, startColumn: 1, endLineNumber: 3, endColumn: 4 }); assert.deepEqual(e.position, { lineNumber: 3, column: 1 }); diff --git a/src/vs/editor/contrib/codelens/codelens.ts b/src/vs/editor/contrib/codelens/codelens.ts index 453b065969f..92821ec5ee6 100644 --- a/src/vs/editor/contrib/codelens/codelens.ts +++ b/src/vs/editor/contrib/codelens/codelens.ts @@ -14,6 +14,7 @@ import { registerLanguageCommand } from 'vs/editor/browser/editorExtensions'; import { CodeLensProviderRegistry, CodeLensProvider, ICodeLensSymbol } from 'vs/editor/common/modes'; import { IModelService } from 'vs/editor/common/services/modelService'; import { asWinJsPromise } from 'vs/base/common/async'; +import { CancellationToken } from 'vs/base/common/cancellation'; export interface ICodeLensData { symbol: ICodeLensSymbol; @@ -58,7 +59,7 @@ export function getCodeLensData(model: ITextModel): TPromise { registerLanguageCommand('_executeCodeLensProvider', function (accessor, args) { - const { resource } = args; + let { resource, itemResolveCount } = args; if (!(resource instanceof URI)) { throw illegalArgument(); } @@ -68,5 +69,22 @@ registerLanguageCommand('_executeCodeLensProvider', function (accessor, args) { throw illegalArgument(); } - return getCodeLensData(model).then(value => value.map(item => item.symbol)); + const result: ICodeLensSymbol[] = []; + return getCodeLensData(model).then(value => { + + let resolve: Thenable[] = []; + + for (const item of value) { + if (typeof itemResolveCount === 'undefined' || Boolean(item.symbol.command)) { + result.push(item.symbol); + } else if (itemResolveCount-- > 0) { + resolve.push(Promise.resolve(item.provider.resolveCodeLens(model, item.symbol, CancellationToken.None)).then(symbol => result.push(symbol))); + } + } + + return Promise.all(resolve); + + }).then(() => { + return result; + }); }); diff --git a/src/vs/editor/contrib/codelens/codelensController.ts b/src/vs/editor/contrib/codelens/codelensController.ts index f3e26373bc6..1c714b60884 100644 --- a/src/vs/editor/contrib/codelens/codelensController.ts +++ b/src/vs/editor/contrib/codelens/codelensController.ts @@ -10,6 +10,7 @@ import { onUnexpectedError } from 'vs/base/common/errors'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { TPromise } from 'vs/base/common/winjs.base'; import { ICommandService } from 'vs/platform/commands/common/commands'; +import { Position } from 'vs/editor/common/core/position'; import * as editorCommon from 'vs/editor/common/editorCommon'; import { CodeLensProviderRegistry, ICodeLensSymbol } from 'vs/editor/common/modes'; import * as editorBrowser from 'vs/editor/browser/editorBrowser'; @@ -218,8 +219,17 @@ export class CodeLensContribution implements editorCommon.IEditorContribution { } } - const centeredRange = this._editor.getCenteredRangeInViewport(); - const shouldRestoreCenteredRange = centeredRange && (groups.length !== this._lenses.length && this._editor.getScrollTop() !== 0); + let visiblePosition: Position = null; + let visiblePositionScrollDelta = 0; + if (this._editor.getScrollTop() !== 0) { + const visibleRanges = this._editor.getVisibleRanges(); + if (visibleRanges.length > 0) { + visiblePosition = visibleRanges[0].getStartPosition(); + const visiblePositionScrollTop = this._editor.getTopForPosition(visiblePosition.lineNumber, visiblePosition.column); + visiblePositionScrollDelta = this._editor.getScrollTop() - visiblePositionScrollTop; + } + } + this._editor.changeDecorations((changeAccessor) => { this._editor.changeViewZones((accessor) => { @@ -259,8 +269,10 @@ export class CodeLensContribution implements editorCommon.IEditorContribution { helper.commit(changeAccessor); }); }); - if (shouldRestoreCenteredRange) { - this._editor.revealRangeInCenter(centeredRange, editorCommon.ScrollType.Immediate); + + if (visiblePosition) { + const visiblePositionScrollTop = this._editor.getTopForPosition(visiblePosition.lineNumber, visiblePosition.column); + this._editor.setScrollTop(visiblePositionScrollTop + visiblePositionScrollDelta); } } diff --git a/src/vs/editor/contrib/colorPicker/color.ts b/src/vs/editor/contrib/colorPicker/color.ts index d064e5e27f3..e3f8d687510 100644 --- a/src/vs/editor/contrib/colorPicker/color.ts +++ b/src/vs/editor/contrib/colorPicker/color.ts @@ -3,10 +3,17 @@ * 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 { ColorProviderRegistry, DocumentColorProvider, IColorInformation, IColorPresentation } from 'vs/editor/common/modes'; import { asWinJsPromise } from 'vs/base/common/async'; import { ITextModel } from 'vs/editor/common/model'; +import { registerLanguageCommand } from 'vs/editor/browser/editorExtensions'; +import { Range, IRange } from 'vs/editor/common/core/range'; +import { illegalArgument } from 'vs/base/common/errors'; +import { IModelService } from 'vs/editor/common/services/modelService'; + export interface IColorData { colorInfo: IColorInformation; @@ -30,3 +37,57 @@ export function getColors(model: ITextModel): TPromise { export function getColorPresentations(model: ITextModel, colorInfo: IColorInformation, provider: DocumentColorProvider): TPromise { return asWinJsPromise(token => provider.provideColorPresentations(model, colorInfo, token)); } + +registerLanguageCommand('_executeDocumentColorProvider', function (accessor, args) { + + const { resource } = args; + if (!(resource instanceof URI)) { + throw illegalArgument(); + } + + const model = accessor.get(IModelService).getModel(resource); + if (!model) { + throw illegalArgument(); + } + + const rawCIs: { range: IRange, color: [number, number, number, number] }[] = []; + const providers = ColorProviderRegistry.ordered(model).reverse(); + const promises = providers.map(provider => asWinJsPromise(token => provider.provideDocumentColors(model, token)).then(result => { + if (Array.isArray(result)) { + for (let ci of result) { + rawCIs.push({ range: ci.range, color: [ci.color.red, ci.color.green, ci.color.blue, ci.color.alpha] }); + } + } + })); + + return TPromise.join(promises).then(() => rawCIs); +}); + + +registerLanguageCommand('_executeColorPresentationProvider', function (accessor, args) { + + const { resource, color, range } = args; + if (!(resource instanceof URI) || !Array.isArray(color) || color.length !== 4 || !Range.isIRange(range)) { + throw illegalArgument(); + } + const [red, green, blue, alpha] = color; + + const model = accessor.get(IModelService).getModel(resource); + if (!model) { + throw illegalArgument(); + } + + const colorInfo = { + range, + color: { red, green, blue, alpha } + }; + + const presentations: IColorPresentation[] = []; + const providers = ColorProviderRegistry.ordered(model).reverse(); + const promises = providers.map(provider => asWinJsPromise(token => provider.provideColorPresentations(model, colorInfo, token)).then(result => { + if (Array.isArray(result)) { + presentations.push(...result); + } + })); + return TPromise.join(promises).then(() => presentations); +}); diff --git a/src/vs/editor/contrib/colorPicker/colorDetector.ts b/src/vs/editor/contrib/colorPicker/colorDetector.ts index b839e90d207..97fbb7b8852 100644 --- a/src/vs/editor/contrib/colorPicker/colorDetector.ts +++ b/src/vs/editor/contrib/colorPicker/colorDetector.ts @@ -16,6 +16,7 @@ import { ColorProviderRegistry } from 'vs/editor/common/modes'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { getColors, IColorData } from 'vs/editor/contrib/colorPicker/color'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; const MAX_DECORATORS = 500; @@ -153,7 +154,7 @@ export class ColorDetector implements IEditorContribution { endLineNumber: c.colorInfo.range.endLineNumber, endColumn: c.colorInfo.range.endColumn }, - options: {} + options: ModelDecorationOptions.EMPTY })); this._decorationsIds = this._editor.deltaDecorations(this._decorationsIds, decorations); diff --git a/src/vs/editor/contrib/colorPicker/colorPickerWidget.ts b/src/vs/editor/contrib/colorPicker/colorPickerWidget.ts index 2bd89430b41..5a8f2a463a7 100644 --- a/src/vs/editor/contrib/colorPicker/colorPickerWidget.ts +++ b/src/vs/editor/contrib/colorPicker/colorPickerWidget.ts @@ -13,7 +13,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { GlobalMouseMoveMonitor, IStandardMouseMoveEventData, standardMouseMoveMerger } from 'vs/base/browser/globalMouseMoveMonitor'; import { Color, RGBA, HSVA } from 'vs/base/common/color'; import { editorHoverBackground } from 'vs/platform/theme/common/colorRegistry'; -import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { registerThemingParticipant, IThemeService } from 'vs/platform/theme/common/themeService'; const $ = dom.$; @@ -23,7 +23,7 @@ export class ColorPickerHeader extends Disposable { private pickedColorNode: HTMLElement; private backgroundColor: Color; - constructor(container: HTMLElement, private model: ColorPickerModel) { + constructor(container: HTMLElement, private model: ColorPickerModel, themeService: IThemeService) { super(); this.domNode = $('.colorpicker-header'); @@ -34,6 +34,7 @@ export class ColorPickerHeader extends Disposable { const colorBox = dom.append(this.domNode, $('.original-color')); colorBox.style.backgroundColor = Color.Format.CSS.format(this.model.originalColor); + this.backgroundColor = themeService.getTheme().getColor(editorHoverBackground) || Color.white; this._register(registerThemingParticipant((theme, collector) => { this.backgroundColor = theme.getColor(editorHoverBackground) || Color.white; })); @@ -332,7 +333,7 @@ export class ColorPickerWidget extends Widget { body: ColorPickerBody; - constructor(container: Node, private model: ColorPickerModel, private pixelRatio: number) { + constructor(container: Node, private model: ColorPickerModel, private pixelRatio: number, themeService: IThemeService) { super(); this._register(onDidChangeZoomLevel(() => this.layout())); @@ -340,7 +341,7 @@ export class ColorPickerWidget extends Widget { const element = $('.colorpicker-widget'); container.appendChild(element); - const header = new ColorPickerHeader(element, this.model); + const header = new ColorPickerHeader(element, this.model, themeService); this.body = new ColorPickerBody(element, this.model, this.pixelRatio); this._register(header); diff --git a/src/vs/editor/contrib/comment/blockCommentCommand.ts b/src/vs/editor/contrib/comment/blockCommentCommand.ts index 3c8caf720d5..a2e51740ebd 100644 --- a/src/vs/editor/contrib/comment/blockCommentCommand.ts +++ b/src/vs/editor/contrib/comment/blockCommentCommand.ts @@ -32,10 +32,24 @@ export class BlockCommentCommand implements editorCommon.ICommand { if (offset + needleLength > haystackLength) { return false; } + for (let i = 0; i < needleLength; i++) { - if (haystack.charCodeAt(offset + i) !== needle.charCodeAt(i)) { - return false; + const codeA = haystack.charCodeAt(offset + i); + const codeB = needle.charCodeAt(i); + + if (codeA === codeB) { + continue; } + if (codeA >= CharCode.A && codeA <= CharCode.Z && codeA + 32 === codeB) { + // codeA is upper-case variant of codeB + continue; + } + if (codeB >= CharCode.A && codeB <= CharCode.Z && codeB + 32 === codeA) { + // codeB is upper-case variant of codeA + continue; + } + + return false; } return true; } diff --git a/src/vs/editor/contrib/comment/lineCommentCommand.ts b/src/vs/editor/contrib/comment/lineCommentCommand.ts index 4a31a0424b7..fd3c9a3912a 100644 --- a/src/vs/editor/contrib/comment/lineCommentCommand.ts +++ b/src/vs/editor/contrib/comment/lineCommentCommand.ts @@ -336,10 +336,10 @@ export class LineCommentCommand implements editorCommon.ICommand { } return new Selection( - result.startLineNumber, - result.startColumn + this._deltaColumn, - result.endLineNumber, - result.endColumn + this._deltaColumn + result.selectionStartLineNumber, + result.selectionStartColumn + this._deltaColumn, + result.positionLineNumber, + result.positionColumn + this._deltaColumn ); } diff --git a/src/vs/editor/contrib/comment/test/lineCommentCommand.test.ts b/src/vs/editor/contrib/comment/test/lineCommentCommand.test.ts index e1e61fa2262..932e73bde2e 100644 --- a/src/vs/editor/contrib/comment/test/lineCommentCommand.test.ts +++ b/src/vs/editor/contrib/comment/test/lineCommentCommand.test.ts @@ -45,6 +45,25 @@ suite('Editor Contrib - Line Comment Command', () => { ); }); + test('case insensitive', function () { + function testLineCommentCommand(lines: string[], selection: Selection, expectedLines: string[], expectedSelection: Selection): void { + let mode = new CommentMode({ lineComment: 'rem' }); + testCommand(lines, mode.getLanguageIdentifier(), selection, (sel) => new LineCommentCommand(sel, 4, Type.Toggle), expectedLines, expectedSelection); + mode.dispose(); + } + + testLineCommentCommand( + [ + 'REM some text' + ], + new Selection(1, 1, 1, 1), + [ + 'some text' + ], + new Selection(1, 1, 1, 1) + ); + }); + function createSimpleModel(lines: string[]): ISimpleModel { return { getLineContent: (lineNumber: number) => { @@ -237,7 +256,7 @@ suite('Editor Contrib - Line Comment Command', () => { '\t!@# some text', '\t!@# some more text' ], - new Selection(1, 1, 2, 2) + new Selection(2, 2, 1, 1) ); }); @@ -252,7 +271,7 @@ suite('Editor Contrib - Line Comment Command', () => { '\t!@# some text', ' !@# some more text' ], - new Selection(1, 1, 2, 2) + new Selection(2, 2, 1, 1) ); }); @@ -271,7 +290,7 @@ suite('Editor Contrib - Line Comment Command', () => { '', '\t!@# some more text' ], - new Selection(1, 1, 4, 2) + new Selection(4, 2, 1, 1) ); }); @@ -288,7 +307,7 @@ suite('Editor Contrib - Line Comment Command', () => { '\t ', '\t\tsome more text' ], - new Selection(1, 1, 3, 2) + new Selection(3, 2, 1, 1) ); }); @@ -305,7 +324,7 @@ suite('Editor Contrib - Line Comment Command', () => { '\t!@# ', '\t\tsome more text' ], - new Selection(1, 1, 3, 1) + new Selection(3, 1, 1, 1) ); }); @@ -350,7 +369,7 @@ suite('Editor Contrib - Line Comment Command', () => { 'first!@#', '\t!@# second line' ], - new Selection(2, 1, 2, 7) + new Selection(2, 7, 2, 1) ); }); @@ -371,7 +390,7 @@ suite('Editor Contrib - Line Comment Command', () => { 'fourth line', 'fifth' ], - new Selection(1, 5, 2, 1) + new Selection(2, 1, 1, 5) ); }); @@ -392,7 +411,7 @@ suite('Editor Contrib - Line Comment Command', () => { 'fourth line', 'fifth' ], - new Selection(1, 5, 2, 8) + new Selection(2, 8, 1, 5) ); }); @@ -413,7 +432,7 @@ suite('Editor Contrib - Line Comment Command', () => { '!@# fourth line', 'fifth' ], - new Selection(3, 5, 4, 8) + new Selection(4, 8, 3, 5) ); }); @@ -474,7 +493,7 @@ suite('Editor Contrib - Line Comment Command', () => { 'fourth line', 'fifth' ], - new Selection(1, 5, 2, 8) + new Selection(2, 8, 1, 5) ); testLineCommentCommand( @@ -493,7 +512,7 @@ suite('Editor Contrib - Line Comment Command', () => { 'fourth line', 'fifth' ], - new Selection(1, 1, 2, 3) + new Selection(2, 3, 1, 1) ); }); @@ -588,6 +607,21 @@ suite('Editor Contrib - Line Comment Command', () => { new Selection(1, 1, 8, 60) ); }); + + test('issue #47004: Toggle comments shouldn\'t move cursor', () => { + testAddLineCommentCommand( + [ + ' A line', + ' Another line' + ], + new Selection(2, 7, 1, 1), + [ + ' !@# A line', + ' !@# Another line' + ], + new Selection(2, 11, 1, 1) + ); + }); }); suite('Editor Contrib - Line Comment As Block Comment', () => { @@ -636,7 +670,7 @@ suite('Editor Contrib - Line Comment As Block Comment', () => { 'fourth line', 'fifth' ], - new Selection(1, 1, 1, 6) + new Selection(1, 6, 1, 1) ); }); @@ -678,7 +712,7 @@ suite('Editor Contrib - Line Comment As Block Comment', () => { 'fourth line', 'fifth' ], - new Selection(1, 5, 3, 2) + new Selection(3, 2, 1, 5) ); testLineCommentCommand( @@ -697,7 +731,7 @@ suite('Editor Contrib - Line Comment As Block Comment', () => { 'fourth line', 'fifth' ], - new Selection(1, 1, 3, 11) + new Selection(3, 11, 1, 1) ); }); }); @@ -823,7 +857,7 @@ suite('Editor Contrib - Line Comment As Block Comment 2', () => { 'fourth line', '\t\tfifth\t\t' ], - new Selection(5, 3, 5, 8) + new Selection(5, 8, 5, 3) ); testLineCommentCommand( @@ -842,7 +876,7 @@ suite('Editor Contrib - Line Comment As Block Comment 2', () => { 'fourth line', '\t\tfifth\t\t' ], - new Selection(5, 3, 5, 8) + new Selection(5, 8, 5, 3) ); testLineCommentCommand( diff --git a/src/vs/editor/contrib/dnd/dnd.ts b/src/vs/editor/contrib/dnd/dnd.ts index 6cc7e1aced4..25a6ccf4363 100644 --- a/src/vs/editor/contrib/dnd/dnd.ts +++ b/src/vs/editor/contrib/dnd/dnd.ts @@ -173,22 +173,17 @@ export class DragAndDropController implements editorCommon.IEditorContribution { }); public showAt(position: Position): void { - this._editor.changeDecorations(changeAccessor => { - let newDecorations: IModelDeltaDecoration[] = []; - newDecorations.push({ - range: new Range(position.lineNumber, position.column, position.lineNumber, position.column), - options: DragAndDropController._DECORATION_OPTIONS - }); + let newDecorations: IModelDeltaDecoration[] = [{ + range: new Range(position.lineNumber, position.column, position.lineNumber, position.column), + options: DragAndDropController._DECORATION_OPTIONS + }]; - this._dndDecorationIds = changeAccessor.deltaDecorations(this._dndDecorationIds, newDecorations); - }); + this._dndDecorationIds = this._editor.deltaDecorations(this._dndDecorationIds, newDecorations); this._editor.revealPosition(position, editorCommon.ScrollType.Immediate); } private _removeDecoration(): void { - this._editor.changeDecorations(changeAccessor => { - changeAccessor.deltaDecorations(this._dndDecorationIds, []); - }); + this._dndDecorationIds = this._editor.deltaDecorations(this._dndDecorationIds, []); } private _hitContent(target: IMouseTarget): boolean { diff --git a/src/vs/editor/contrib/find/findController.ts b/src/vs/editor/contrib/find/findController.ts index 1b01a612ecf..1150a26fac9 100644 --- a/src/vs/editor/contrib/find/findController.ts +++ b/src/vs/editor/contrib/find/findController.ts @@ -446,8 +446,8 @@ export class StartFindWithSelectionAction extends EditorAction { constructor() { super({ id: FIND_IDS.StartFindWithSelection, - label: nls.localize('startFindAction', "Find"), - alias: 'Find', + label: nls.localize('startFindWithSelectionAction', "Find With Selection"), + alias: 'Find With Selection', precondition: null, kbOpts: { kbExpr: null, diff --git a/src/vs/editor/contrib/find/findDecorations.ts b/src/vs/editor/contrib/find/findDecorations.ts index 84ddb11b262..ff1c795d2e3 100644 --- a/src/vs/editor/contrib/find/findDecorations.ts +++ b/src/vs/editor/contrib/find/findDecorations.ts @@ -267,6 +267,7 @@ export class FindDecorations implements IDisposable { private static readonly _CURRENT_FIND_MATCH_DECORATION = ModelDecorationOptions.register({ stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, + zIndex: 13, className: 'currentFindMatch', showIfCollapsed: true, overviewRuler: { diff --git a/src/vs/editor/contrib/find/findWidget.ts b/src/vs/editor/contrib/find/findWidget.ts index a852de93cbc..83ff2daa50a 100644 --- a/src/vs/editor/contrib/find/findWidget.ts +++ b/src/vs/editor/contrib/find/findWidget.ts @@ -1066,48 +1066,50 @@ export class SimpleButton extends Widget { // theming registerThemingParticipant((theme, collector) => { - function addBackgroundColorRule(selector: string, color: Color): void { + const addBackgroundColorRule = (selector: string, color: Color): void => { if (color) { collector.addRule(`.monaco-editor ${selector} { background-color: ${color}; }`); } - } + }; addBackgroundColorRule('.findMatch', theme.getColor(editorFindMatchHighlight)); addBackgroundColorRule('.currentFindMatch', theme.getColor(editorFindMatch)); addBackgroundColorRule('.findScope', theme.getColor(editorFindRangeHighlight)); - let widgetBackground = theme.getColor(editorWidgetBackground); + const widgetBackground = theme.getColor(editorWidgetBackground); addBackgroundColorRule('.find-widget', widgetBackground); - let widgetShadowColor = theme.getColor(widgetShadow); + const widgetShadowColor = theme.getColor(widgetShadow); if (widgetShadowColor) { collector.addRule(`.monaco-editor .find-widget { box-shadow: 0 2px 8px ${widgetShadowColor}; }`); } - let findMatchHighlightBorder = theme.getColor(editorFindMatchHighlightBorder); + const findMatchHighlightBorder = theme.getColor(editorFindMatchHighlightBorder); if (findMatchHighlightBorder) { - collector.addRule(`.monaco-editor .findMatch { border: 1px dotted ${findMatchHighlightBorder}; -moz-box-sizing: border-box; box-sizing: border-box; }`); - } - let findMatchBorder = theme.getColor(editorFindMatchBorder); - if (findMatchBorder) { - collector.addRule(`.monaco-editor .currentFindMatch { border: 2px solid ${findMatchBorder}; padding: 1px; -moz-box-sizing: border-box; box-sizing: border-box; }`); - } - let findRangeHighlightBorder = theme.getColor(editorFindRangeHighlightBorder); - if (findRangeHighlightBorder) { - collector.addRule(`.monaco-editor .findScope { border: 1px dashed ${findRangeHighlightBorder}; }`); + collector.addRule(`.monaco-editor .findMatch { border: 1px ${theme.type === 'hc' ? 'dotted' : 'solid'} ${findMatchHighlightBorder}; box-sizing: border-box; }`); } - let hcBorder = theme.getColor(contrastBorder); + const findMatchBorder = theme.getColor(editorFindMatchBorder); + if (findMatchBorder) { + collector.addRule(`.monaco-editor .currentFindMatch { border: 2px solid ${findMatchBorder}; padding: 1px; box-sizing: border-box; }`); + } + + const findRangeHighlightBorder = theme.getColor(editorFindRangeHighlightBorder); + if (findRangeHighlightBorder) { + collector.addRule(`.monaco-editor .findScope { border: 1px ${theme.type === 'hc' ? 'dashed' : 'solid'} ${findRangeHighlightBorder}; }`); + } + + const hcBorder = theme.getColor(contrastBorder); if (hcBorder) { collector.addRule(`.monaco-editor .find-widget { border: 2px solid ${hcBorder}; }`); } - let error = theme.getColor(errorForeground); + const error = theme.getColor(errorForeground); if (error) { collector.addRule(`.monaco-editor .find-widget.no-results .matchesCount { color: ${error}; }`); } - let border = theme.getColor(editorWidgetBorder); + const border = theme.getColor(editorWidgetBorder); if (border) { collector.addRule(`.monaco-editor .find-widget .monaco-sash { background-color: ${border}; width: 3px !important; margin-left: -4px;}`); } diff --git a/src/vs/editor/contrib/folding/folding.ts b/src/vs/editor/contrib/folding/folding.ts index b9c2f1131bf..4d54fef449f 100644 --- a/src/vs/editor/contrib/folding/folding.ts +++ b/src/vs/editor/contrib/folding/folding.ts @@ -2,10 +2,10 @@ * 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!./folding'; import * as nls from 'vs/nls'; import * as types from 'vs/base/common/types'; import { escapeRegExpCharacters } from 'vs/base/common/strings'; @@ -28,7 +28,7 @@ import { IRange } from 'vs/editor/common/core/range'; import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; import { IndentRangeProvider } from 'vs/editor/contrib/folding/indentRangeProvider'; import { IPosition } from 'vs/editor/common/core/position'; -import { FoldingProviderRegistry, FoldingRangeType } from 'vs/editor/common/modes'; +import { FoldingRangeProviderRegistry, FoldingRangeKind } from 'vs/editor/common/modes'; import { SyntaxRangeProvider } from './syntaxRangeProvider'; import { CancellationToken } from 'vs/base/common/cancellation'; @@ -50,6 +50,7 @@ export class FoldingController implements IEditorContribution { private editor: ICodeEditor; private _isEnabled: boolean; private _autoHideFoldingControls: boolean; + private _useFoldingProviders: boolean; private foldingDecorationProvider: FoldingDecorationProvider; @@ -72,6 +73,7 @@ export class FoldingController implements IEditorContribution { this.editor = editor; this._isEnabled = this.editor.getConfiguration().contribInfo.folding; this._autoHideFoldingControls = this.editor.getConfiguration().contribInfo.showFoldingControls === 'mouseover'; + this._useFoldingProviders = this.editor.getConfiguration().contribInfo.foldingStrategy !== 'indentation'; this.globalToDispose = []; this.localToDispose = []; @@ -80,7 +82,7 @@ export class FoldingController implements IEditorContribution { this.foldingDecorationProvider.autoHideFoldingControls = this._autoHideFoldingControls; this.globalToDispose.push(this.editor.onDidChangeModel(() => this.onModelChanged())); - this.globalToDispose.push(FoldingProviderRegistry.onDidChange(() => this.onModelChanged())); + this.globalToDispose.push(FoldingRangeProviderRegistry.onDidChange(() => this.onFoldingStrategyChanged())); this.globalToDispose.push(this.editor.onDidChangeConfiguration((e: IConfigurationChangedEvent) => { if (e.contribInfo) { @@ -95,6 +97,11 @@ export class FoldingController implements IEditorContribution { this.foldingDecorationProvider.autoHideFoldingControls = this._autoHideFoldingControls; this.onModelContentChanged(); } + let oldUseFoldingProviders = this._useFoldingProviders; + this._useFoldingProviders = this.editor.getConfiguration().contribInfo.foldingStrategy !== 'indentation'; + if (oldUseFoldingProviders !== this._useFoldingProviders) { + this.onFoldingStrategyChanged(); + } } })); this.globalToDispose.push({ dispose: () => dispose(this.localToDispose) }); @@ -190,11 +197,16 @@ export class FoldingController implements IEditorContribution { this.onModelContentChanged(); } + private onFoldingStrategyChanged() { + this.rangeProvider = null; + this.onModelContentChanged(); + } + private getRangeProvider(): RangeProvider { if (!this.rangeProvider) { - let foldingProviders = FoldingProviderRegistry.ordered(this.foldingModel.textModel); - if (foldingProviders.length) { - this.rangeProvider = new SyntaxRangeProvider(foldingProviders); + if (this._useFoldingProviders) { + let foldingProviders = FoldingRangeProviderRegistry.ordered(this.foldingModel.textModel); + this.rangeProvider = foldingProviders.length ? new SyntaxRangeProvider(foldingProviders) : new IndentRangeProvider(); } else { this.rangeProvider = new IndentRangeProvider(); } @@ -572,7 +584,7 @@ class FoldAllBlockCommentsAction extends FoldingAction { invoke(foldingController: FoldingController, foldingModel: FoldingModel, editor: ICodeEditor): void { if (foldingModel.regions.hasTypes()) { - setCollapseStateForType(foldingModel, FoldingRangeType.Comment, true); + setCollapseStateForType(foldingModel, FoldingRangeKind.Comment.value, true); } else { let comments = LanguageConfigurationRegistry.getComments(editor.getModel().getLanguageIdentifier().id); if (comments && comments.blockCommentStartToken) { @@ -600,7 +612,7 @@ class FoldAllRegionsAction extends FoldingAction { invoke(foldingController: FoldingController, foldingModel: FoldingModel, editor: ICodeEditor): void { if (foldingModel.regions.hasTypes()) { - setCollapseStateForType(foldingModel, FoldingRangeType.Region, true); + setCollapseStateForType(foldingModel, FoldingRangeKind.Region.value, true); } else { let foldingRules = LanguageConfigurationRegistry.getFoldingRules(editor.getModel().getLanguageIdentifier().id); if (foldingRules && foldingRules.markers && foldingRules.markers.start) { @@ -628,7 +640,7 @@ class UnfoldAllRegionsAction extends FoldingAction { invoke(foldingController: FoldingController, foldingModel: FoldingModel, editor: ICodeEditor): void { if (foldingModel.regions.hasTypes()) { - setCollapseStateForType(foldingModel, FoldingRangeType.Region, false); + setCollapseStateForType(foldingModel, FoldingRangeKind.Region.value, false); } else { let foldingRules = LanguageConfigurationRegistry.getFoldingRules(editor.getModel().getLanguageIdentifier().id); if (foldingRules && foldingRules.markers && foldingRules.markers.start) { diff --git a/src/vs/editor/contrib/folding/foldingDecorations.ts b/src/vs/editor/contrib/folding/foldingDecorations.ts index fcc392cb184..17359b411fb 100644 --- a/src/vs/editor/contrib/folding/foldingDecorations.ts +++ b/src/vs/editor/contrib/folding/foldingDecorations.ts @@ -10,18 +10,18 @@ import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; export class FoldingDecorationProvider implements IDecorationProvider { - private COLLAPSED_VISUAL_DECORATION = ModelDecorationOptions.register({ + private static COLLAPSED_VISUAL_DECORATION = ModelDecorationOptions.register({ stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, afterContentClassName: 'inline-folded', linesDecorationsClassName: 'folding collapsed' }); - private EXPANDED_AUTO_HIDE_VISUAL_DECORATION = ModelDecorationOptions.register({ + private static EXPANDED_AUTO_HIDE_VISUAL_DECORATION = ModelDecorationOptions.register({ stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, linesDecorationsClassName: 'folding' }); - private EXPANDED_VISUAL_DECORATION = ModelDecorationOptions.register({ + private static EXPANDED_VISUAL_DECORATION = ModelDecorationOptions.register({ stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, linesDecorationsClassName: 'folding alwaysShowFoldIcons' }); @@ -33,11 +33,11 @@ export class FoldingDecorationProvider implements IDecorationProvider { getDecorationOption(isCollapsed: boolean): ModelDecorationOptions { if (isCollapsed) { - return this.COLLAPSED_VISUAL_DECORATION; + return FoldingDecorationProvider.COLLAPSED_VISUAL_DECORATION; } else if (this.autoHideFoldingControls) { - return this.EXPANDED_AUTO_HIDE_VISUAL_DECORATION; + return FoldingDecorationProvider.EXPANDED_AUTO_HIDE_VISUAL_DECORATION; } else { - return this.EXPANDED_VISUAL_DECORATION; + return FoldingDecorationProvider.EXPANDED_VISUAL_DECORATION; } } diff --git a/src/vs/editor/contrib/folding/hiddenRangeModel.ts b/src/vs/editor/contrib/folding/hiddenRangeModel.ts index 2ebd17b2acf..0caf73939a8 100644 --- a/src/vs/editor/contrib/folding/hiddenRangeModel.ts +++ b/src/vs/editor/contrib/folding/hiddenRangeModel.ts @@ -8,7 +8,7 @@ import { Range, IRange } from 'vs/editor/common/core/range'; import { FoldingModel, CollapseMemento } from 'vs/editor/contrib/folding/foldingModel'; import { IDisposable } from 'vs/base/common/lifecycle'; import { Selection } from 'vs/editor/common/core/selection'; -import { findFirst } from 'vs/base/common/arrays'; +import { findFirstInSorted } from 'vs/base/common/arrays'; export class HiddenRangeModel { private _foldingModel: FoldingModel; @@ -149,7 +149,7 @@ function isInside(line: number, range: IRange) { return line >= range.startLineNumber && line <= range.endLineNumber; } function findRange(ranges: IRange[], line: number): IRange { - let i = findFirst(ranges, r => line < r.startLineNumber) - 1; + let i = findFirstInSorted(ranges, r => line < r.startLineNumber) - 1; if (i >= 0 && ranges[i].endLineNumber >= line) { return ranges[i]; } diff --git a/src/vs/editor/contrib/folding/syntaxRangeProvider.ts b/src/vs/editor/contrib/folding/syntaxRangeProvider.ts index 7d5d7447929..eec4d8406c9 100644 --- a/src/vs/editor/contrib/folding/syntaxRangeProvider.ts +++ b/src/vs/editor/contrib/folding/syntaxRangeProvider.ts @@ -5,7 +5,7 @@ 'use strict'; -import { FoldingProvider, IFoldingRange, FoldingContext } from 'vs/editor/common/modes'; +import { FoldingRangeProvider, FoldingRange, FoldingContext } from 'vs/editor/common/modes'; import { onUnexpectedExternalError } from 'vs/base/common/errors'; import { toThenable } from 'vs/base/common/async'; import { ITextModel } from 'vs/editor/common/model'; @@ -16,7 +16,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; const MAX_FOLDING_REGIONS_FOR_INDENT_LIMIT = 5000; -export interface IFoldingRangeData extends IFoldingRange { +export interface IFoldingRangeData extends FoldingRange { rank: number; } @@ -26,7 +26,7 @@ const foldingContext: FoldingContext = { export class SyntaxRangeProvider implements RangeProvider { - constructor(private providers: FoldingProvider[]) { + constructor(private providers: FoldingRangeProvider[]) { } compute(model: ITextModel, cancellationToken: CancellationToken): Thenable { @@ -41,23 +41,23 @@ export class SyntaxRangeProvider implements RangeProvider { } -function collectSyntaxRanges(providers: FoldingProvider[], model: ITextModel, cancellationToken: CancellationToken): Thenable { +function collectSyntaxRanges(providers: FoldingRangeProvider[], model: ITextModel, cancellationToken: CancellationToken): Thenable { let promises = providers.map(provider => toThenable(provider.provideFoldingRanges(model, foldingContext, cancellationToken))); - return TPromise.join(promises).then(lists => { + return TPromise.join(promises).then(results => { let rangeData: IFoldingRangeData[] = null; if (cancellationToken.isCancellationRequested) { return null; } - for (let i = 0; i < lists.length; i++) { - let list = lists[i]; - if (list && Array.isArray(list.ranges)) { + for (let i = 0; i < results.length; i++) { + let ranges = results[i]; + if (Array.isArray(ranges)) { if (!Array.isArray(rangeData)) { rangeData = []; } let nLines = model.getLineCount(); - for (let r of list.ranges) { - if (r.startLineNumber > 0 && r.endLineNumber > r.startLineNumber && r.endLineNumber <= nLines) { - rangeData.push({ startLineNumber: r.startLineNumber, endLineNumber: r.endLineNumber, rank: i, type: r.type }); + for (let r of ranges) { + if (r.start > 0 && r.end > r.start && r.end <= nLines) { + rangeData.push({ start: r.start, end: r.end, rank: i, kind: r.kind }); } } } @@ -145,7 +145,7 @@ export class RangesCollector { export function sanitizeRanges(rangeData: IFoldingRangeData[]): FoldingRegions { let sorted = rangeData.sort((d1, d2) => { - let diff = d1.startLineNumber - d2.startLineNumber; + let diff = d1.start - d2.start; if (diff === 0) { diff = d1.rank - d2.rank; } @@ -158,20 +158,20 @@ export function sanitizeRanges(rangeData: IFoldingRangeData[]): FoldingRegions { for (let entry of sorted) { if (!top) { top = entry; - collector.add(entry.startLineNumber, entry.endLineNumber, entry.type, previous.length); + collector.add(entry.start, entry.end, entry.kind && entry.kind.value, previous.length); } else { - if (entry.startLineNumber > top.startLineNumber) { - if (entry.endLineNumber <= top.endLineNumber) { + if (entry.start > top.start) { + if (entry.end <= top.end) { previous.push(top); top = entry; - collector.add(entry.startLineNumber, entry.endLineNumber, entry.type, previous.length); - } else if (entry.startLineNumber > top.endLineNumber) { + collector.add(entry.start, entry.end, entry.kind && entry.kind.value, previous.length); + } else if (entry.start > top.end) { do { top = previous.pop(); - } while (top && entry.startLineNumber > top.endLineNumber); + } while (top && entry.start > top.end); previous.push(top); top = entry; - collector.add(entry.startLineNumber, entry.endLineNumber, entry.type, previous.length); + collector.add(entry.start, entry.end, entry.kind && entry.kind.value, previous.length); } } } diff --git a/src/vs/editor/contrib/format/format.ts b/src/vs/editor/contrib/format/format.ts index 3d448d4ae06..bc6ed0f89d9 100644 --- a/src/vs/editor/contrib/format/format.ts +++ b/src/vs/editor/contrib/format/format.ts @@ -38,14 +38,14 @@ export function getDocumentRangeFormattingEdits(model: ITextModel, range: Range, let result: TextEdit[]; return sequence(providers.map(provider => { - if (isFalsyOrEmpty(result)) { - return () => { - return asWinJsPromise(token => provider.provideDocumentRangeFormattingEdits(model, range, options, token)).then(value => { - result = value; - }, onUnexpectedExternalError); - }; - } - return undefined; + return () => { + if (!isFalsyOrEmpty(result)) { + return undefined; + } + return asWinJsPromise(token => provider.provideDocumentRangeFormattingEdits(model, range, options, token)).then(value => { + result = value; + }, onUnexpectedExternalError); + }; })).then(() => result); } @@ -59,14 +59,14 @@ export function getDocumentFormattingEdits(model: ITextModel, options: Formattin let result: TextEdit[]; return sequence(providers.map(provider => { - if (isFalsyOrEmpty(result)) { - return () => { - return asWinJsPromise(token => provider.provideDocumentFormattingEdits(model, options, token)).then(value => { - result = value; - }, onUnexpectedExternalError); - }; - } - return undefined; + return () => { + if (!isFalsyOrEmpty(result)) { + return undefined; + } + return asWinJsPromise(token => provider.provideDocumentFormattingEdits(model, options, token)).then(value => { + result = value; + }, onUnexpectedExternalError); + }; })).then(() => result); } diff --git a/src/vs/editor/contrib/format/formatActions.ts b/src/vs/editor/contrib/format/formatActions.ts index b7801c7b2c2..1344aec8661 100644 --- a/src/vs/editor/contrib/format/formatActions.ts +++ b/src/vs/editor/contrib/format/formatActions.ts @@ -161,7 +161,7 @@ class FormatOnType implements editorCommon.IEditorContribution { return; } - EditOperationsCommand.execute(this.editor, edits, true); + EditOperationsCommand.executeAsCommand(this.editor, edits); alertFormattingEdits(edits); }, (err) => { @@ -244,7 +244,7 @@ class FormatOnPaste implements editorCommon.IEditorContribution { if (!state.validate(this.editor) || isFalsyOrEmpty(edits)) { return; } - EditOperationsCommand.execute(this.editor, edits, false); + EditOperationsCommand.execute(this.editor, edits); alertFormattingEdits(edits); }); } @@ -280,12 +280,12 @@ export abstract class AbstractFormatAction extends EditorAction { return; } - EditOperationsCommand.execute(editor, edits, false); + EditOperationsCommand.execute(editor, edits); alertFormattingEdits(edits); editor.focus(); }, err => { if (err instanceof Error && err.name === NoProviderError.Name) { - notificationService.info(nls.localize('no.provider', "There is no formatter for '{0}'-files installed.", editor.getModel().getLanguageIdentifier().language)); + this._notifyNoProviderError(notificationService, editor.getModel().getLanguageIdentifier().language); } else { throw err; } @@ -293,6 +293,9 @@ export abstract class AbstractFormatAction extends EditorAction { } protected abstract _getFormattingEdits(editor: ICodeEditor): TPromise; + protected _notifyNoProviderError(notificationService: INotificationService, language: string): void { + notificationService.info(nls.localize('no.provider', "There is no formatter for '{0}'-files installed.", language)); + } } export class FormatDocumentAction extends AbstractFormatAction { @@ -322,6 +325,10 @@ export class FormatDocumentAction extends AbstractFormatAction { const { tabSize, insertSpaces } = model.getOptions(); return getDocumentFormattingEdits(model, { tabSize, insertSpaces }); } + + protected _notifyNoProviderError(notificationService: INotificationService, language: string): void { + notificationService.info(nls.localize('no.documentprovider', "There is no document formatter for '{0}'-files installed.", language)); + } } export class FormatSelectionAction extends AbstractFormatAction { @@ -349,6 +356,10 @@ export class FormatSelectionAction extends AbstractFormatAction { const { tabSize, insertSpaces } = model.getOptions(); return getDocumentRangeFormattingEdits(model, editor.getSelection(), { tabSize, insertSpaces }); } + + protected _notifyNoProviderError(notificationService: INotificationService, language: string): void { + notificationService.info(nls.localize('no.selectionprovider', "There is no selection formatter for '{0}'-files installed.", language)); + } } registerEditorContribution(FormatOnType); diff --git a/src/vs/editor/contrib/format/formatCommand.ts b/src/vs/editor/contrib/format/formatCommand.ts index 42f320e538f..5b38129e3ae 100644 --- a/src/vs/editor/contrib/format/formatCommand.ts +++ b/src/vs/editor/contrib/format/formatCommand.ts @@ -15,39 +15,60 @@ import { EditOperation } from 'vs/editor/common/core/editOperation'; export class EditOperationsCommand implements editorCommon.ICommand { - static execute(editor: ICodeEditor, edits: TextEdit[], asCommand: boolean) { - const cmd = new EditOperationsCommand(edits, editor.getSelection()); - if (typeof cmd._newEol === 'number') { - editor.getModel().setEOL(cmd._newEol); + static _handleEolEdits(editor: ICodeEditor, edits: TextEdit[]): ISingleEditOperation[] { + let newEol: EndOfLineSequence = undefined; + let singleEdits: ISingleEditOperation[] = []; + + for (let edit of edits) { + if (typeof edit.eol === 'number') { + newEol = edit.eol; + } + if (edit.range && typeof edit.text === 'string') { + singleEdits.push(edit); + } } + + if (typeof newEol === 'number') { + editor.getModel().setEOL(newEol); + } + + return singleEdits; + } + + static executeAsCommand(editor: ICodeEditor, _edits: TextEdit[]) { + let edits = this._handleEolEdits(editor, _edits); + const cmd = new EditOperationsCommand(edits, editor.getSelection()); editor.pushUndoStop(); - if (!asCommand) { - editor.executeEdits('formatEditsCommand', cmd._edits.map(edit => EditOperation.replace(Range.lift(edit.range), edit.text))); + editor.executeCommand('formatEditsCommand', cmd); + editor.pushUndoStop(); + } + + static isFullModelReplaceEdit(editor: ICodeEditor, edit: ISingleEditOperation): boolean { + const model = editor.getModel(); + const editRange = model.validateRange(edit.range); + const fullModelRange = model.getFullModelRange(); + return fullModelRange.equalsRange(editRange); + } + + static execute(editor: ICodeEditor, _edits: TextEdit[]) { + let edits = this._handleEolEdits(editor, _edits); + editor.pushUndoStop(); + if (edits.length === 1 && EditOperationsCommand.isFullModelReplaceEdit(editor, edits[0])) { + // We use replace semantics and hope that markers stay put... + editor.executeEdits('formatEditsCommand', edits.map(edit => EditOperation.replace(Range.lift(edit.range), edit.text))); } else { - editor.executeCommand('formatEditsCommand', cmd); + editor.executeEdits('formatEditsCommand', edits.map(edit => EditOperation.replaceMove(Range.lift(edit.range), edit.text))); } editor.pushUndoStop(); } - private _edits: TextEdit[]; - private _newEol: EndOfLineSequence; - + private _edits: ISingleEditOperation[]; private _initialSelection: Selection; private _selectionId: string; - constructor(edits: TextEdit[], initialSelection: Selection) { + constructor(edits: ISingleEditOperation[], initialSelection: Selection) { this._initialSelection = initialSelection; - this._edits = []; - this._newEol = undefined; - - for (let edit of edits) { - if (typeof edit.eol === 'number') { - this._newEol = edit.eol; - } - if (edit.range && typeof edit.text === 'string') { - this._edits.push(edit); - } - } + this._edits = edits; } public getEditOperations(model: ITextModel, builder: editorCommon.IEditOperationBuilder): void { diff --git a/src/vs/editor/contrib/format/test/formatCommand.test.ts b/src/vs/editor/contrib/format/test/formatCommand.test.ts index 5b1022fa070..0a9bbde4593 100644 --- a/src/vs/editor/contrib/format/test/formatCommand.test.ts +++ b/src/vs/editor/contrib/format/test/formatCommand.test.ts @@ -11,6 +11,7 @@ import { ISingleEditOperation } from 'vs/editor/common/model'; import { TextModel } from 'vs/editor/common/model/textModel'; import { EditOperationsCommand } from 'vs/editor/contrib/format/formatCommand'; import { testCommand } from 'vs/editor/test/browser/testCommand'; +import { withTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; function editOp(startLineNumber: number, startColumn: number, endLineNumber: number, endColumn: number, text: string[]): ISingleEditOperation { return { @@ -310,4 +311,52 @@ suite('FormatCommand', () => { ); }); + test('issue #44870', () => { + const initialText = [ + '[', + ' {},{', + ' }', + ']', + ]; + withTestCodeEditor(initialText, {}, (editor) => { + editor.setSelection(new Selection(2, 8, 2, 8)); + EditOperationsCommand.execute(editor, [ + editOp(2, 8, 2, 8, ['', ' ']), + editOp(2, 9, 3, 5, ['']), + ]); + assert.equal(editor.getValue(), [ + '[', + ' {},', + ' {}', + ']', + ].join('\n')); + assert.deepEqual(editor.getSelection(), new Selection(3, 5, 3, 5)); + }); + }); + + test('issue #47382: full model replace moves cursor to end of file', () => { + const initialText = [ + 'just some', + 'Text', + '...more text' + ]; + withTestCodeEditor(initialText, {}, (editor) => { + editor.setSelection(new Selection(2, 1, 2, 1)); + EditOperationsCommand.execute(editor, [{ + range: new Range(1, 1, 3, 13), + text: [ + 'just some', + '\tText', + '...more text' + ].join('\n') + }]); + assert.equal(editor.getValue(), [ + 'just some', + '\tText', + '...more text' + ].join('\n')); + assert.deepEqual(editor.getSelection(), new Selection(2, 1, 2, 1)); + }); + }); + }); diff --git a/src/vs/editor/contrib/gotoError/gotoError.ts b/src/vs/editor/contrib/gotoError/gotoError.ts index 06c17fa4a3a..e9aa3c4667d 100644 --- a/src/vs/editor/contrib/gotoError/gotoError.ts +++ b/src/vs/editor/contrib/gotoError/gotoError.ts @@ -17,7 +17,6 @@ import { Range } from 'vs/editor/common/core/range'; import * as editorCommon from 'vs/editor/common/editorCommon'; import { registerEditorAction, registerEditorContribution, ServicesAccessor, IActionOptions, EditorAction, EditorCommand, registerEditorCommand } from 'vs/editor/browser/editorExtensions'; import { ICodeEditor, isCodeEditor } from 'vs/editor/browser/editorBrowser'; -import { } from 'vs/platform/theme/common/colorRegistry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; @@ -26,6 +25,7 @@ import { compare } from 'vs/base/common/strings'; import { binarySearch } from 'vs/base/common/arrays'; import { IEditorService } from 'vs/platform/editor/common/editor'; import { TPromise } from 'vs/base/common/winjs.base'; +import { onUnexpectedError } from 'vs/base/common/errors'; class MarkerModel { @@ -233,11 +233,18 @@ class MarkerController implements editorCommon.IEditorContribution { this._model = new MarkerModel(this._editor, markers); this._markerService.onMarkerChanged(this._onMarkerChanged, this, this._disposeOnClose); - this._widget = new MarkerNavigationWidget(this._editor, this._themeService, this._editorService); + this._widget = new MarkerNavigationWidget(this._editor, this._themeService); this._widgetVisible.set(true); this._disposeOnClose.push(this._model); this._disposeOnClose.push(this._widget); + this._disposeOnClose.push(this._widget.onDidSelectRelatedInformation(related => { + this._editorService.openEditor({ + resource: related.resource, + options: { pinned: true, revealIfOpened: true, selection: Range.lift(related).collapseToStart() } + }).then(undefined, onUnexpectedError); + this.closeMarkersNavigation(false); + })); this._disposeOnClose.push(this._editor.onDidChangeModel(() => this._cleanUp())); this._disposeOnClose.push(this._model.onCurrentMarkerChanged(marker => { @@ -261,9 +268,11 @@ class MarkerController implements editorCommon.IEditorContribution { return this._model; } - public closeMarkersNavigation(): void { + public closeMarkersNavigation(focusEditor: boolean = true): void { this._cleanUp(); - this._editor.focus(); + if (focusEditor) { + this._editor.focus(); + } } private _onMarkerChanged(changedResources: URI[]): void { @@ -276,7 +285,7 @@ class MarkerController implements editorCommon.IEditorContribution { private _getMarkers(): IMarker[] { return this._markerService.read({ resource: this._editor.getModel().uri, - severities: MarkerSeverity.Error | MarkerSeverity.Warning + severities: MarkerSeverity.Error | MarkerSeverity.Warning | MarkerSeverity.Info }); } } @@ -306,7 +315,7 @@ class MarkerNavigationAction extends EditorAction { } // try with the next/prev file - let markers = markerService.read({ severities: MarkerSeverity.Error | MarkerSeverity.Warning }).sort(MarkerNavigationAction.compareMarker); + let markers = markerService.read({ severities: MarkerSeverity.Error | MarkerSeverity.Warning | MarkerSeverity.Info }).sort(MarkerNavigationAction.compareMarker); if (markers.length === 0) { return undefined; } diff --git a/src/vs/editor/contrib/gotoError/gotoErrorWidget.ts b/src/vs/editor/contrib/gotoError/gotoErrorWidget.ts index 0e3ac5d116d..4166d476ab5 100644 --- a/src/vs/editor/contrib/gotoError/gotoErrorWidget.ts +++ b/src/vs/editor/contrib/gotoError/gotoErrorWidget.ts @@ -22,10 +22,9 @@ import { editorErrorForeground, editorErrorBorder, editorWarningForeground, edit import { ScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; import { ScrollbarVisibility } from 'vs/base/common/scrollable'; import { ScrollType } from 'vs/editor/common/editorCommon'; -import { getBaseLabel } from 'vs/base/common/labels'; -import { IEditorService } from 'vs/platform/editor/common/editor'; -import { onUnexpectedError } from 'vs/base/common/errors'; +import { getBaseLabel, getPathLabel } from 'vs/base/common/labels'; import { isFalsyOrEmpty } from 'vs/base/common/arrays'; +import { Event, Emitter } from 'vs/base/common/event'; class MessageWidget { @@ -39,7 +38,7 @@ class MessageWidget { private readonly _relatedDiagnostics = new WeakMap(); private readonly _disposables: IDisposable[] = []; - constructor(parent: HTMLElement, editor: ICodeEditor, editorService: IEditorService) { + constructor(parent: HTMLElement, editor: ICodeEditor, onRelatedInformation: (related: IRelatedInformation) => void, ) { this._editor = editor; const domNode = document.createElement('div'); @@ -56,10 +55,7 @@ class MessageWidget { event.preventDefault(); const related = this._relatedDiagnostics.get(event.target); if (related) { - editorService.openEditor({ - resource: related.resource, - options: { pinned: true, revealIfOpened: true, selection: Range.lift(related).collapseToStart() } - }).then(undefined, onUnexpectedError); + onRelatedInformation(related); } })); @@ -114,6 +110,7 @@ class MessageWidget { let relatedResource = document.createElement('span'); dom.addClass(relatedResource, 'filename'); relatedResource.innerHTML = `${getBaseLabel(related.resource)}(${related.startLineNumber}, ${related.startColumn}): `; + relatedResource.title = getPathLabel(related.resource); this._relatedDiagnostics.set(relatedResource, related); let relatedMessage = document.createElement('span'); @@ -148,11 +145,13 @@ export class MarkerNavigationWidget extends ZoneWidget { private _callOnDispose: IDisposable[] = []; private _severity: MarkerSeverity; private _backgroundColor: Color; + private _onDidSelectRelatedInformation = new Emitter(); + + readonly onDidSelectRelatedInformation: Event = this._onDidSelectRelatedInformation.event; constructor( editor: ICodeEditor, - private _themeService: IThemeService, - private _editorService: IEditorService + private _themeService: IThemeService ) { super(editor, { showArrow: true, showFrame: true, isAccessible: true }); this._severity = MarkerSeverity.Warning; @@ -208,7 +207,7 @@ export class MarkerNavigationWidget extends ZoneWidget { this._title.className = 'block title'; this._container.appendChild(this._title); - this._message = new MessageWidget(this._container, this.editor, this._editorService); + this._message = new MessageWidget(this._container, this.editor, related => this._onDidSelectRelatedInformation.fire(related)); this._disposables.push(this._message); } diff --git a/src/vs/editor/contrib/hover/hover.ts b/src/vs/editor/contrib/hover/hover.ts index abab5279934..a48e61cb54f 100644 --- a/src/vs/editor/contrib/hover/hover.ts +++ b/src/vs/editor/contrib/hover/hover.ts @@ -19,7 +19,7 @@ import { ICodeEditor, IEditorMouseEvent, MouseTargetType } from 'vs/editor/brows import { ModesContentHoverWidget } from './modesContentHover'; import { ModesGlyphHoverWidget } from './modesGlyphHover'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { registerThemingParticipant, IThemeService } from 'vs/platform/theme/common/themeService'; import { editorHoverHighlight, editorHoverBackground, editorHoverBorder, textLinkForeground, textCodeBlockBackground } from 'vs/platform/theme/common/colorRegistry'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { MarkdownRenderer } from 'vs/editor/contrib/markdown/markdownRenderer'; @@ -57,7 +57,8 @@ export class ModesHoverController implements editorCommon.IEditorContribution { constructor(editor: ICodeEditor, @IOpenerService private readonly _openerService: IOpenerService, - @IModeService private readonly _modeService: IModeService + @IModeService private readonly _modeService: IModeService, + @IThemeService private readonly _themeService: IThemeService ) { this._editor = editor; @@ -159,7 +160,7 @@ export class ModesHoverController implements editorCommon.IEditorContribution { private _createHoverWidget() { const renderer = new MarkdownRenderer(this._editor, this._modeService, this._openerService); - this._contentWidget = new ModesContentHoverWidget(this._editor, renderer); + this._contentWidget = new ModesContentHoverWidget(this._editor, renderer, this._themeService); this._glyphWidget = new ModesGlyphHoverWidget(this._editor, renderer); } @@ -189,7 +190,13 @@ class ShowHoverAction extends EditorAction { constructor() { super({ id: 'editor.action.showHover', - label: nls.localize('showHover', "Show Hover"), + label: nls.localize({ + key: 'showHover', + comment: [ + 'Label for action that will trigger the showing of a hover in the editor.', + 'This allows for users to show the hover without using the mouse.' + ] + }, "Show Hover"), alias: 'Show Hover', precondition: null, kbOpts: { diff --git a/src/vs/editor/contrib/hover/modesContentHover.ts b/src/vs/editor/contrib/hover/modesContentHover.ts index 186c999c232..36b6e611927 100644 --- a/src/vs/editor/contrib/hover/modesContentHover.ts +++ b/src/vs/editor/contrib/hover/modesContentHover.ts @@ -23,6 +23,7 @@ import { ColorDetector } from 'vs/editor/contrib/colorPicker/colorDetector'; import { Color, RGBA } from 'vs/base/common/color'; import { IDisposable, empty as EmptyDisposable, dispose, combinedDisposable } from 'vs/base/common/lifecycle'; import { getColorPresentations } from 'vs/editor/contrib/colorPicker/color'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; const $ = dom.$; class ColorHover { @@ -169,7 +170,11 @@ export class ModesContentHoverWidget extends ContentHoverWidget { private renderDisposable: IDisposable = EmptyDisposable; private toDispose: IDisposable[] = []; - constructor(editor: ICodeEditor, markdownRenderner: MarkdownRenderer) { + constructor( + editor: ICodeEditor, + markdownRenderner: MarkdownRenderer, + private readonly _themeService: IThemeService + ) { super(ModesContentHoverWidget.ID, editor); this._computer = new ModesContentComputer(this._editor); @@ -331,7 +336,7 @@ export class ModesContentHoverWidget extends ContentHoverWidget { // create blank olor picker model and widget first to ensure it's positioned correctly. const model = new ColorPickerModel(color, [], 0); - const widget = new ColorPickerWidget(fragment, model, this._editor.getConfiguration().pixelRatio); + const widget = new ColorPickerWidget(fragment, model, this._editor.getConfiguration().pixelRatio, this._themeService); getColorPresentations(editorModel, colorInfo, msg.provider).then(colorPresentations => { model.colorPresentations = colorPresentations; diff --git a/src/vs/editor/contrib/inPlaceReplace/inPlaceReplace.ts b/src/vs/editor/contrib/inPlaceReplace/inPlaceReplace.ts index 0d8c406b5e7..18548f6eccc 100644 --- a/src/vs/editor/contrib/inPlaceReplace/inPlaceReplace.ts +++ b/src/vs/editor/contrib/inPlaceReplace/inPlaceReplace.ts @@ -10,7 +10,6 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; -import { IModelDecorationsChangeAccessor } from 'vs/editor/common/model'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { registerEditorAction, ServicesAccessor, EditorAction, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; import { IInplaceReplaceSupportResult } from 'vs/editor/common/modes'; @@ -131,9 +130,7 @@ class InPlaceReplaceController implements IEditorContribution { this.decorationRemover.cancel(); this.decorationRemover = TPromise.timeout(350); this.decorationRemover.then(() => { - this.editor.changeDecorations((accessor: IModelDecorationsChangeAccessor) => { - this.decorationIds = accessor.deltaDecorations(this.decorationIds, []); - }); + this.decorationIds = this.editor.deltaDecorations(this.decorationIds, []); }); }); } diff --git a/src/vs/editor/contrib/indentation/indentation.ts b/src/vs/editor/contrib/indentation/indentation.ts index 859db38bb06..3ca805ebfad 100644 --- a/src/vs/editor/contrib/indentation/indentation.ts +++ b/src/vs/editor/contrib/indentation/indentation.ts @@ -586,18 +586,27 @@ function getIndentationEditOperations(model: ITextModel, builder: IEditOperation spaces += ' '; } - const content = model.getLinesContent(); - for (let i = 0; i < content.length; i++) { - let lastIndentationColumn = model.getLineFirstNonWhitespaceColumn(i + 1); + let spacesRegExp = new RegExp(spaces, 'gi'); + + for (let lineNumber = 1, lineCount = model.getLineCount(); lineNumber <= lineCount; lineNumber++) { + let lastIndentationColumn = model.getLineFirstNonWhitespaceColumn(lineNumber); if (lastIndentationColumn === 0) { - lastIndentationColumn = model.getLineMaxColumn(i + 1); + lastIndentationColumn = model.getLineMaxColumn(lineNumber); } - const text = (tabsToSpaces ? content[i].substr(0, lastIndentationColumn).replace(/\t/ig, spaces) : - content[i].substr(0, lastIndentationColumn).replace(new RegExp(spaces, 'gi'), '\t')) + - content[i].substr(lastIndentationColumn); + if (lastIndentationColumn === 1) { + continue; + } - builder.addEditOperation(new Range(i + 1, 1, i + 1, model.getLineMaxColumn(i + 1)), text); + const originalIndentationRange = new Range(lineNumber, 1, lineNumber, lastIndentationColumn); + const originalIndentation = model.getValueInRange(originalIndentationRange); + const newIndentation = ( + tabsToSpaces + ? originalIndentation.replace(/\t/ig, spaces) + : originalIndentation.replace(spacesRegExp, '\t') + ); + + builder.addEditOperation(originalIndentationRange, newIndentation); } } diff --git a/src/vs/editor/contrib/indentation/test/indentation.test.ts b/src/vs/editor/contrib/indentation/test/indentation.test.ts index 1f2e9ff1577..59ece7d34e7 100644 --- a/src/vs/editor/contrib/indentation/test/indentation.test.ts +++ b/src/vs/editor/contrib/indentation/test/indentation.test.ts @@ -57,7 +57,7 @@ suite('Editor Contrib - Indentation to Spaces', () => { 'fourth line', 'fifth' ], - new Selection(1, 5, 1, 5) + new Selection(1, 9, 1, 9) ); }); @@ -79,7 +79,7 @@ suite('Editor Contrib - Indentation to Spaces', () => { ' fourth line', 'fifth' ], - new Selection(1, 5, 1, 5) + new Selection(1, 7, 1, 7) ); }); @@ -157,7 +157,7 @@ suite('Editor Contrib - Indentation to Tabs', () => { ' fourth line', 'fifth' ], - new Selection(1, 5, 1, 5), + new Selection(1, 8, 1, 8), 2, [ '\t\t\tfirst ', @@ -169,4 +169,18 @@ suite('Editor Contrib - Indentation to Tabs', () => { new Selection(1, 5, 1, 5) ); }); + + test('issue #45996', function () { + testIndentationToSpacesCommand( + [ + '\tabc', + ], + new Selection(1, 3, 1, 3), + 4, + [ + ' abc', + ], + new Selection(1, 6, 1, 6) + ); + }); }); diff --git a/src/vs/editor/contrib/linesOperations/test/linesOperations.test.ts b/src/vs/editor/contrib/linesOperations/test/linesOperations.test.ts index 6c3284eb35b..d7ed7bcbf60 100644 --- a/src/vs/editor/contrib/linesOperations/test/linesOperations.test.ts +++ b/src/vs/editor/contrib/linesOperations/test/linesOperations.test.ts @@ -8,12 +8,12 @@ import * as assert from 'assert'; import { Selection } from 'vs/editor/common/core/selection'; import { Position } from 'vs/editor/common/core/position'; import { Handler } from 'vs/editor/common/editorCommon'; -import { ITextModel, DefaultEndOfLine } from 'vs/editor/common/model'; +import { ITextModel } from 'vs/editor/common/model'; import { withTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; import { DeleteAllLeftAction, JoinLinesAction, TransposeAction, UpperCaseAction, LowerCaseAction, DeleteAllRightAction, InsertLineBeforeAction, InsertLineAfterAction, IndentLinesAction, SortLinesAscendingAction, SortLinesDescendingAction } from 'vs/editor/contrib/linesOperations/linesOperations'; import { Cursor } from 'vs/editor/common/controller/cursor'; -import { TextModel } from 'vs/editor/common/model/textModel'; import { CoreEditingCommands } from 'vs/editor/browser/controller/coreCommands'; +import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; suite('Editor Contrib - Line Operations', () => { suite('SortLinesAscendingAction', () => { @@ -746,17 +746,12 @@ suite('Editor Contrib - Line Operations', () => { test('Bug 18276:[editor] Indentation broken when selection is empty', () => { - let model = TextModel.createFromString( + let model = createTextModel( [ 'function baz() {' ].join('\n'), { - isForSimpleWidget: false, - defaultEOL: DefaultEndOfLine.LF, - detectIndentation: false, insertSpaces: false, - tabSize: 4, - trimAutoWhitespace: true } ); diff --git a/src/vs/editor/contrib/links/links.ts b/src/vs/editor/contrib/links/links.ts index 61e679cb177..b9814039159 100644 --- a/src/vs/editor/contrib/links/links.ts +++ b/src/vs/editor/contrib/links/links.ts @@ -38,8 +38,17 @@ const HOVER_MESSAGE_COMMAND_META = new MarkdownString().appendText( : nls.localize('links.command', "Ctrl + click to execute command") ); -const HOVER_MESSAGE_GENERAL_ALT = new MarkdownString().appendText(nls.localize('links.navigate.al', "Alt + click to follow link")); -const HOVER_MESSAGE_COMMAND_ALT = new MarkdownString().appendText(nls.localize('links.command.al', "Alt + click to execute command")); +const HOVER_MESSAGE_GENERAL_ALT = new MarkdownString().appendText( + platform.isMacintosh + ? nls.localize('links.navigate.al.mac', "Option + click to follow link") + : nls.localize('links.navigate.al', "Alt + click to follow link") +); + +const HOVER_MESSAGE_COMMAND_ALT = new MarkdownString().appendText( + platform.isMacintosh + ? nls.localize('links.command.al.mac', "Option + click to execute command") + : nls.localize('links.command.al', "Alt + click to execute command") +); const decoration = { meta: ModelDecorationOptions.register({ @@ -242,32 +251,30 @@ class LinkDetector implements editorCommon.IEditorContribution { private updateDecorations(links: Link[]): void { const useMetaKey = (this.editor.getConfiguration().multiCursorModifier === 'altKey'); - this.editor.changeDecorations((changeAccessor: IModelDecorationsChangeAccessor) => { - var oldDecorations: string[] = []; - let keys = Object.keys(this.currentOccurrences); - for (let i = 0, len = keys.length; i < len; i++) { - let decorationId = keys[i]; - let occurance = this.currentOccurrences[decorationId]; - oldDecorations.push(occurance.decorationId); - } + let oldDecorations: string[] = []; + let keys = Object.keys(this.currentOccurrences); + for (let i = 0, len = keys.length; i < len; i++) { + let decorationId = keys[i]; + let occurance = this.currentOccurrences[decorationId]; + oldDecorations.push(occurance.decorationId); + } - var newDecorations: IModelDeltaDecoration[] = []; - if (links) { - // Not sure why this is sometimes null - for (var i = 0; i < links.length; i++) { - newDecorations.push(LinkOccurrence.decoration(links[i], useMetaKey)); - } + let newDecorations: IModelDeltaDecoration[] = []; + if (links) { + // Not sure why this is sometimes null + for (let i = 0; i < links.length; i++) { + newDecorations.push(LinkOccurrence.decoration(links[i], useMetaKey)); } + } - var decorations = changeAccessor.deltaDecorations(oldDecorations, newDecorations); + let decorations = this.editor.deltaDecorations(oldDecorations, newDecorations); - this.currentOccurrences = {}; - this.activeLinkDecorationId = null; - for (let i = 0, len = decorations.length; i < len; i++) { - var occurance = new LinkOccurrence(links[i], decorations[i]); - this.currentOccurrences[occurance.decorationId] = occurance; - } - }); + this.currentOccurrences = {}; + this.activeLinkDecorationId = null; + for (let i = 0, len = decorations.length; i < len; i++) { + let occurance = new LinkOccurrence(links[i], decorations[i]); + this.currentOccurrences[occurance.decorationId] = occurance; + } } private _onEditorMouseMove(mouseEvent: ClickLinkMouseEvent, withKey?: ClickLinkKeyboardEvent): void { diff --git a/src/vs/editor/contrib/message/messageController.ts b/src/vs/editor/contrib/message/messageController.ts index 90982184a1c..e80d0f6b90c 100644 --- a/src/vs/editor/contrib/message/messageController.ts +++ b/src/vs/editor/contrib/message/messageController.ts @@ -6,9 +6,10 @@ 'use strict'; import 'vs/css!./messageController'; +import * as nls from 'vs/nls'; import { setDisposableTimeout } from 'vs/base/common/async'; import { KeyCode } from 'vs/base/common/keyCodes'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { IDisposable, dispose, Disposable } from 'vs/base/common/lifecycle'; import { alert } from 'vs/base/browser/ui/aria/aria'; import { Range } from 'vs/editor/common/core/range'; import * as editorCommon from 'vs/editor/common/editorCommon'; @@ -20,11 +21,11 @@ import { registerThemingParticipant, HIGH_CONTRAST } from 'vs/platform/theme/com import { inputValidationInfoBorder, inputValidationInfoBackground } from 'vs/platform/theme/common/colorRegistry'; import { KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; -export class MessageController implements editorCommon.IEditorContribution { +export class MessageController extends Disposable implements editorCommon.IEditorContribution { private static readonly _id = 'editor.contrib.messageController'; - static CONTEXT_SNIPPET_MODE = new RawContextKey('messageVisible', false); + static MESSAGE_VISIBLE = new RawContextKey('messageVisible', false); static get(editor: ICodeEditor): MessageController { return editor.getContribution(MessageController._id); @@ -43,11 +44,14 @@ export class MessageController implements editorCommon.IEditorContribution { editor: ICodeEditor, @IContextKeyService contextKeyService: IContextKeyService ) { + super(); this._editor = editor; - this._visible = MessageController.CONTEXT_SNIPPET_MODE.bindTo(contextKeyService); + this._visible = MessageController.MESSAGE_VISIBLE.bindTo(contextKeyService); + this._register(this._editor.onDidAttemptReadOnlyEdit(() => this._onDidAttemptReadOnlyEdit())); } dispose(): void { + super.dispose(); this._visible.reset(); } @@ -96,6 +100,10 @@ export class MessageController implements editorCommon.IEditorContribution { this._messageListeners = dispose(this._messageListeners); this._messageListeners.push(MessageWidget.fadeOut(this._messageWidget)); } + + private _onDidAttemptReadOnlyEdit(): void { + this.showMessage(nls.localize('editor.readonly', "Cannot edit in read-only editor"), this._editor.getPosition()); + } } const MessageCommand = EditorCommand.bindToContribution(MessageController.get); @@ -103,7 +111,7 @@ const MessageCommand = EditorCommand.bindToContribution(Messa registerEditorCommand(new MessageCommand({ id: 'leaveEditorMessage', - precondition: MessageController.CONTEXT_SNIPPET_MODE, + precondition: MessageController.MESSAGE_VISIBLE, handler: c => c.closeMessage(), kbOpts: { weight: KeybindingsRegistry.WEIGHT.editorContrib(30), diff --git a/src/vs/editor/contrib/multicursor/multicursor.ts b/src/vs/editor/contrib/multicursor/multicursor.ts index 6561c8b9ab6..f13b5d6167f 100644 --- a/src/vs/editor/contrib/multicursor/multicursor.ts +++ b/src/vs/editor/contrib/multicursor/multicursor.ts @@ -113,29 +113,25 @@ class InsertCursorAtEndOfEachLineSelected extends EditorAction { }); } - private getCursorsForSelection(selection: Selection, editor: ICodeEditor): Selection[] { + private getCursorsForSelection(selection: Selection, model: ITextModel, result: Selection[]): void { if (selection.isEmpty()) { - return []; + return; } - let model = editor.getModel(); - let newSelections: Selection[] = []; for (let i = selection.startLineNumber; i < selection.endLineNumber; i++) { let currentLineMaxColumn = model.getLineMaxColumn(i); - newSelections.push(new Selection(i, currentLineMaxColumn, i, currentLineMaxColumn)); + result.push(new Selection(i, currentLineMaxColumn, i, currentLineMaxColumn)); } if (selection.endColumn > 1) { - newSelections.push(new Selection(selection.endLineNumber, selection.endColumn, selection.endLineNumber, selection.endColumn)); + result.push(new Selection(selection.endLineNumber, selection.endColumn, selection.endLineNumber, selection.endColumn)); } - - return newSelections; } public run(accessor: ServicesAccessor, editor: ICodeEditor): void { - let selections = editor.getSelections(); - let newSelections = selections - .map((selection) => this.getCursorsForSelection(selection, editor)) - .reduce((prev, curr) => { return prev.concat(curr); }); + const model = editor.getModel(); + const selections = editor.getSelections(); + let newSelections: Selection[] = []; + selections.forEach((sel) => this.getCursorsForSelection(sel, model, newSelections)); if (newSelections.length > 0) { editor.setSelections(newSelections); @@ -803,9 +799,7 @@ export class SelectionHighlighter extends Disposable implements IEditorContribut this.state = state; if (!this.state) { - if (this.decorations.length > 0) { - this.decorations = this.editor.deltaDecorations(this.decorations, []); - } + this.decorations = this.editor.deltaDecorations(this.decorations, []); return; } diff --git a/src/vs/editor/contrib/parameterHints/parameterHintsWidget.ts b/src/vs/editor/contrib/parameterHints/parameterHintsWidget.ts index 0724631045a..2c874d77fd5 100644 --- a/src/vs/editor/contrib/parameterHints/parameterHintsWidget.ts +++ b/src/vs/editor/contrib/parameterHints/parameterHintsWidget.ts @@ -50,6 +50,7 @@ export class ParameterHintsModel extends Disposable { private triggerCharactersListeners: IDisposable[]; private active: boolean; private throttledDelayer: RunOnceScheduler; + private provideSignatureHelpRequest?: TPromise; constructor(editor: ICodeEditor) { super(); @@ -80,6 +81,11 @@ export class ParameterHintsModel extends Disposable { if (!silent) { this._onCancel.fire(void 0); } + + if (this.provideSignatureHelpRequest) { + this.provideSignatureHelpRequest.cancel(); + this.provideSignatureHelpRequest = undefined; + } } trigger(delay = ParameterHintsModel.DELAY): void { @@ -92,7 +98,11 @@ export class ParameterHintsModel extends Disposable { } private doTrigger(): void { - provideSignatureHelp(this.editor.getModel(), this.editor.getPosition()) + if (this.provideSignatureHelpRequest) { + this.provideSignatureHelpRequest.cancel(); + } + + this.provideSignatureHelpRequest = provideSignatureHelp(this.editor.getModel(), this.editor.getPosition()) .then(null, onUnexpectedError) .then(result => { if (!result || !result.signatures || result.signatures.length === 0) { diff --git a/src/vs/editor/contrib/parameterHints/test/parameterHintsModel.test.ts b/src/vs/editor/contrib/parameterHints/test/parameterHintsModel.test.ts new file mode 100644 index 00000000000..4dce80655b2 --- /dev/null +++ b/src/vs/editor/contrib/parameterHints/test/parameterHintsModel.test.ts @@ -0,0 +1,103 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import URI from 'vs/base/common/uri'; +import { Position } from 'vs/editor/common/core/position'; +import { ITextModel } from 'vs/editor/common/model'; +import { TextModel } from 'vs/editor/common/model/textModel'; +import { SignatureHelp, SignatureHelpProvider, SignatureHelpProviderRegistry } from 'vs/editor/common/modes'; +import { MockScopeLocation, TestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService'; +import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; +import { IStorageService, NullStorageService } from 'vs/platform/storage/common/storage'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; +import { ParameterHintsModel } from '../parameterHintsWidget'; + +function createMockEditor(model: TextModel): TestCodeEditor { + const contextKeyService = new MockContextKeyService(); + const telemetryService = NullTelemetryService; + const notificationService = new TestNotificationService(); + const instantiationService = new InstantiationService(new ServiceCollection( + [IContextKeyService, contextKeyService], + [ITelemetryService, telemetryService], + [IStorageService, NullStorageService], + [INotificationService, TestNotificationService] + )); + + const editor = new TestCodeEditor(new MockScopeLocation(), {}, false, instantiationService, contextKeyService, notificationService); + editor.setModel(model); + return editor; +} + + +suite('ParameterHintsModel', () => { + let disposables: IDisposable[] = []; + + + setup(function () { + disposables = dispose(disposables); + }); + + test('Should cancel existing request when new request comes in', () => { + const textModel = TextModel.createFromString('abc def', undefined, undefined, URI.parse('test:somefile.ttt')); + disposables.push(textModel); + + const editor = createMockEditor(textModel); + const hintsModel = new ParameterHintsModel(editor); + + let didRequestCancellationOf = -1; + let invokeCount = 0; + const longRunningProvider = new class implements SignatureHelpProvider { + signatureHelpTriggerCharacters: string[] = []; + + provideSignatureHelp(model: ITextModel, position: Position, token: CancellationToken): SignatureHelp | Thenable { + const count = invokeCount++; + token.onCancellationRequested(() => { didRequestCancellationOf = count; }); + + // retrigger on first request + if (count === 0) { + hintsModel.trigger(0); + } + + return new Promise(resolve => { + setTimeout(() => { + resolve({ + signatures: [{ + label: '' + count, + parameters: [] + }], + activeParameter: 0, + activeSignature: 0 + }); + }, 100); + }); + } + }; + + disposables.push(SignatureHelpProviderRegistry.register({ scheme: 'test' }, longRunningProvider)); + + hintsModel.trigger(0); + assert.strictEqual(-1, didRequestCancellationOf); + + return new Promise((resolve, reject) => + hintsModel.onHint(e => { + try { + assert.strictEqual(0, didRequestCancellationOf); + assert.strictEqual('1', e.hints.signatures[0].label); + resolve(); + } catch (e) { + reject(e); + } + })); + }); +}); diff --git a/src/vs/editor/contrib/referenceSearch/peekViewWidget.ts b/src/vs/editor/contrib/referenceSearch/peekViewWidget.ts index 593f6b9e3fd..6c7a42c84aa 100644 --- a/src/vs/editor/contrib/referenceSearch/peekViewWidget.ts +++ b/src/vs/editor/contrib/referenceSearch/peekViewWidget.ts @@ -133,7 +133,7 @@ export abstract class PeekViewWidget extends ZoneWidget { const actionsContainer = $('.peekview-actions').appendTo(this._headElement); const actionBarOptions = this._getActionBarOptions(); - this._actionbarWidget = new ActionBar(actionsContainer, actionBarOptions); + this._actionbarWidget = new ActionBar(actionsContainer.getHTMLElement(), actionBarOptions); this._disposables.push(this._actionbarWidget); this._actionbarWidget.push(new Action('peekview.close', nls.localize('label.close', "Close"), 'close-peekview-action', true, () => { diff --git a/src/vs/editor/contrib/referenceSearch/referenceSearch.ts b/src/vs/editor/contrib/referenceSearch/referenceSearch.ts index a8510002d8c..90169cab7f1 100644 --- a/src/vs/editor/contrib/referenceSearch/referenceSearch.ts +++ b/src/vs/editor/contrib/referenceSearch/referenceSearch.ts @@ -206,6 +206,18 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ } }); +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'goToNextReferenceFromEmbeddedEditor', + weight: KeybindingsRegistry.WEIGHT.editorContrib(50), + primary: KeyCode.F4, + when: PeekContext.inPeekEditor, + handler(accessor) { + withController(accessor, controller => { + controller.goToNextOrPreviousReference(true); + }); + } +}); + KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'goToPreviousReference', weight: KeybindingsRegistry.WEIGHT.workbenchContrib(50), @@ -218,6 +230,18 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ } }); +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'goToPreviousReferenceFromEmbeddedEditor', + weight: KeybindingsRegistry.WEIGHT.editorContrib(50), + primary: KeyMod.Shift | KeyCode.F4, + when: PeekContext.inPeekEditor, + handler(accessor) { + withController(accessor, controller => { + controller.goToNextOrPreviousReference(false); + }); + } +}); + KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'closeReferenceSearch', weight: KeybindingsRegistry.WEIGHT.workbenchContrib(50), diff --git a/src/vs/editor/contrib/referenceSearch/referencesController.ts b/src/vs/editor/contrib/referenceSearch/referencesController.ts index a06529eecdf..51d82fbc443 100644 --- a/src/vs/editor/contrib/referenceSearch/referencesController.ts +++ b/src/vs/editor/contrib/referenceSearch/referencesController.ts @@ -16,7 +16,6 @@ import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace import { IStorageService } from 'vs/platform/storage/common/storage'; import * as editorCommon from 'vs/editor/common/editorCommon'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; import { ReferencesModel } from './referencesModel'; import { ReferenceWidget, LayoutData } from './referencesWidget'; import { Range } from 'vs/editor/common/core/range'; @@ -34,7 +33,7 @@ export interface RequestOptions { onGoto?: (reference: Location) => TPromise; } -export class ReferencesController implements editorCommon.IEditorContribution { +export abstract class ReferencesController implements editorCommon.IEditorContribution { private static readonly ID = 'editor.contrib.referencesController'; @@ -52,6 +51,7 @@ export class ReferencesController implements editorCommon.IEditorContribution { } public constructor( + private _defaultTreeKeyboardSupport: boolean, editor: ICodeEditor, @IContextKeyService contextKeyService: IContextKeyService, @IEditorService private readonly _editorService: IEditorService, @@ -103,7 +103,7 @@ export class ReferencesController implements editorCommon.IEditorContribution { })); const storageKey = 'peekViewLayout'; const data = JSON.parse(this._storageService.get(storageKey, undefined, '{}')); - this._widget = new ReferenceWidget(this._editor, data, this._textModelResolverService, this._contextService, this._themeService, this._instantiationService, this._environmentService); + this._widget = new ReferenceWidget(this._editor, this._defaultTreeKeyboardSupport, data, this._textModelResolverService, this._contextService, this._themeService, this._instantiationService, this._environmentService); this._widget.setTitle(nls.localize('labelLoading', "Loading...")); this._widget.show(range); this._disposables.push(this._widget.onDidClose(() => { @@ -155,16 +155,17 @@ export class ReferencesController implements editorCommon.IEditorContribution { // show widget return this._widget.setModel(this._model).then(() => { + if (this._widget) { // might have been closed + // set title + this._widget.setMetaTitle(options.getMetaTitle(this._model)); - // set title - this._widget.setMetaTitle(options.getMetaTitle(this._model)); - - // set 'best' selection - let uri = this._editor.getModel().uri; - let pos = new Position(range.startLineNumber, range.startColumn); - let selection = this._model.nearestReference(uri, pos); - if (selection) { - return this._widget.setSelection(selection); + // set 'best' selection + let uri = this._editor.getModel().uri; + let pos = new Position(range.startLineNumber, range.startColumn); + let selection = this._model.nearestReference(uri, pos); + if (selection) { + return this._widget.setSelection(selection); + } } return undefined; }); @@ -175,13 +176,15 @@ export class ReferencesController implements editorCommon.IEditorContribution { } public async goToNextOrPreviousReference(fwd: boolean) { - let source = this._model.nearestReference(this._editor.getModel().uri, this._widget.position); - let target = this._model.nextOrPreviousReference(source, fwd); - let editorFocus = this._editor.isFocused(); - await this._widget.setSelection(target); - await this._gotoReference(target); - if (editorFocus) { - this._editor.focus(); + if (this._model) { // can be called while still resolving... + let source = this._model.nearestReference(this._editor.getModel().uri, this._widget.position); + let target = this._model.nextOrPreviousReference(source, fwd); + let editorFocus = this._editor.isFocused(); + await this._widget.setSelection(target); + await this._gotoReference(target); + if (editorFocus) { + this._editor.focus(); + } } } @@ -246,5 +249,3 @@ export class ReferencesController implements editorCommon.IEditorContribution { } } } - -registerEditorContribution(ReferencesController); diff --git a/src/vs/editor/contrib/referenceSearch/referencesModel.ts b/src/vs/editor/contrib/referenceSearch/referencesModel.ts index 88880e33a97..81865714ad8 100644 --- a/src/vs/editor/contrib/referenceSearch/referencesModel.ts +++ b/src/vs/editor/contrib/referenceSearch/referencesModel.ts @@ -261,7 +261,7 @@ export class ReferencesModel implements IDisposable { let childCount = parent.children.length; let groupCount = parent.parent.groups.length; - if (groupCount === 1 || next && idx + 1 < childCount || !next && idx > 1) { + if (groupCount === 1 || next && idx + 1 < childCount || !next && idx > 0) { // cycling within one file if (next) { idx = (idx + 1) % childCount; diff --git a/src/vs/editor/contrib/referenceSearch/referencesWidget.ts b/src/vs/editor/contrib/referenceSearch/referencesWidget.ts index 5a1a4c97e27..3e98e35c67e 100644 --- a/src/vs/editor/contrib/referenceSearch/referencesWidget.ts +++ b/src/vs/editor/contrib/referenceSearch/referencesWidget.ts @@ -84,28 +84,25 @@ class DecorationsManager implements IDisposable { private _addDecorations(reference: FileReferences): void { this._callOnModelChange.push(this._editor.getModel().onDidChangeDecorations((event) => this._onDecorationChanged())); - this._editor.changeDecorations(accessor => { + const newDecorations: IModelDeltaDecoration[] = []; + const newDecorationsActualIndex: number[] = []; - const newDecorations: IModelDeltaDecoration[] = []; - const newDecorationsActualIndex: number[] = []; - - for (let i = 0, len = reference.children.length; i < len; i++) { - let oneReference = reference.children[i]; - if (this._decorationIgnoreSet.has(oneReference.id)) { - continue; - } - newDecorations.push({ - range: oneReference.range, - options: DecorationsManager.DecorationOptions - }); - newDecorationsActualIndex.push(i); + for (let i = 0, len = reference.children.length; i < len; i++) { + let oneReference = reference.children[i]; + if (this._decorationIgnoreSet.has(oneReference.id)) { + continue; } + newDecorations.push({ + range: oneReference.range, + options: DecorationsManager.DecorationOptions + }); + newDecorationsActualIndex.push(i); + } - const decorations = accessor.deltaDecorations([], newDecorations); - for (let i = 0; i < decorations.length; i++) { - this._decorations.set(decorations[i], reference.children[newDecorationsActualIndex[i]]); - } - }); + const decorations = this._editor.deltaDecorations([], newDecorations); + for (let i = 0; i < decorations.length; i++) { + this._decorations.set(decorations[i], reference.children[newDecorationsActualIndex[i]]); + } } private _onDecorationChanged(): void { @@ -143,21 +140,19 @@ class DecorationsManager implements IDisposable { } }); - this._editor.changeDecorations((accessor) => { - for (let i = 0, len = toRemove.length; i < len; i++) { - this._decorations.delete(toRemove[i]); - } - accessor.deltaDecorations(toRemove, []); - }); + for (let i = 0, len = toRemove.length; i < len; i++) { + this._decorations.delete(toRemove[i]); + } + this._editor.deltaDecorations(toRemove, []); } public removeDecorations(): void { - this._editor.changeDecorations(accessor => { - this._decorations.forEach((value, key) => { - accessor.removeDecoration(key); - }); - this._decorations.clear(); + let toRemove: string[] = []; + this._decorations.forEach((value, key) => { + toRemove.push(key); }); + this._editor.deltaDecorations(toRemove, []); + this._decorations.clear(); } } @@ -533,6 +528,7 @@ export class ReferenceWidget extends PeekViewWidget { constructor( editor: ICodeEditor, + private _defaultTreeKeyboardSupport: boolean, public layoutData: LayoutData, private _textModelResolverService: ITextModelService, private _contextService: IWorkspaceContextService, @@ -635,7 +631,7 @@ export class ReferenceWidget extends PeekViewWidget { // tree container.div({ 'class': 'ref-tree inline' }, (div: Builder) => { - var controller = this._instantiationService.createInstance(Controller, { clickBehavior: ClickBehavior.ON_MOUSE_UP /* our controller already deals with this */ }); + var controller = this._instantiationService.createInstance(Controller, { keyboardSupport: this._defaultTreeKeyboardSupport, clickBehavior: ClickBehavior.ON_MOUSE_UP /* our controller already deals with this */ }); this._callOnDispose.push(controller); var config = { @@ -658,7 +654,7 @@ export class ReferenceWidget extends PeekViewWidget { var onEvent = (element: any, kind: 'show' | 'goto' | 'side') => { if (element instanceof OneReference) { if (kind === 'show') { - this._revealReference(element); + this._revealReference(element, false); } this._onDidSelectReference.fire({ element, kind, source: 'tree' }); } @@ -710,7 +706,7 @@ export class ReferenceWidget extends PeekViewWidget { } public setSelection(selection: OneReference): TPromise { - return this._revealReference(selection).then(() => { + return this._revealReference(selection, true).then(() => { // show in tree this._tree.setSelection([selection]); @@ -780,7 +776,7 @@ export class ReferenceWidget extends PeekViewWidget { return undefined; } - private _revealReference(reference: OneReference): TPromise { + private async _revealReference(reference: OneReference, revealParent: boolean): TPromise { // Update widget header if (reference.uri.scheme !== Schemas.inMemory) { @@ -791,6 +787,10 @@ export class ReferenceWidget extends PeekViewWidget { const promise = this._textModelResolverService.createModelReference(reference.uri); + if (revealParent) { + await this._tree.reveal(reference.parent); + } + return TPromise.join([promise, this._tree.reveal(reference)]).then(values => { const ref = values[0]; @@ -836,6 +836,7 @@ export const peekViewEditorGutterBackground = registerColor('peekViewEditorGutte export const peekViewResultsMatchHighlight = registerColor('peekViewResult.matchHighlightBackground', { dark: '#ea5c004d', light: '#ea5c004d', hc: null }, nls.localize('peekViewResultsMatchHighlight', 'Match highlight color in the peek view result list.')); export const peekViewEditorMatchHighlight = registerColor('peekViewEditor.matchHighlightBackground', { dark: '#ff8f0099', light: '#f5d802de', hc: null }, nls.localize('peekViewEditorMatchHighlight', 'Match highlight color in the peek view editor.')); +export const peekViewEditorMatchHighlightBorder = registerColor('peekViewEditor.matchHighlightBorder', { dark: null, light: null, hc: activeContrastBorder }, nls.localize('peekViewEditorMatchHighlightBorder', 'Match highlight border in the peek view editor.')); registerThemingParticipant((theme, collector) => { @@ -847,10 +848,13 @@ registerThemingParticipant((theme, collector) => { if (referenceHighlightColor) { collector.addRule(`.monaco-editor .reference-zone-widget .preview .reference-decoration { background-color: ${referenceHighlightColor}; }`); } + let referenceHighlightBorder = theme.getColor(peekViewEditorMatchHighlightBorder); + if (referenceHighlightBorder) { + collector.addRule(`.monaco-editor .reference-zone-widget .preview .reference-decoration { border: 2px solid ${referenceHighlightBorder}; box-sizing: border-box; }`); + } let hcOutline = theme.getColor(activeContrastBorder); if (hcOutline) { collector.addRule(`.monaco-editor .reference-zone-widget .ref-tree .referenceMatch { border: 1px dotted ${hcOutline}; box-sizing: border-box; }`); - collector.addRule(`.monaco-editor .reference-zone-widget .preview .reference-decoration { border: 2px solid ${hcOutline}; box-sizing: border-box; }`); } let resultsBackground = theme.getColor(peekViewResultsBackground); if (resultsBackground) { diff --git a/src/vs/editor/contrib/rename/rename.ts b/src/vs/editor/contrib/rename/rename.ts index 70341d1b55e..803d4ac0b8c 100644 --- a/src/vs/editor/contrib/rename/rename.ts +++ b/src/vs/editor/contrib/rename/rename.ts @@ -23,7 +23,7 @@ import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { optional } from 'vs/platform/instantiation/common/instantiation'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { asWinJsPromise } from 'vs/base/common/async'; -import { WorkspaceEdit, RenameProviderRegistry, RenameContext, RenameProvider } from 'vs/editor/common/modes'; +import { WorkspaceEdit, RenameProviderRegistry, RenameProvider, RenameLocation } from 'vs/editor/common/modes'; import { Position } from 'vs/editor/common/core/position'; import { alert } from 'vs/base/browser/ui/aria/aria'; import { Range } from 'vs/editor/common/core/range'; @@ -47,29 +47,29 @@ class RenameSkeleton { return this._provider.length > 0; } - async resolveRenameInformation(): TPromise { + async resolveRenameLocation(): TPromise { let [provider] = this._provider; - let information: RenameContext; + let res: RenameLocation; if (provider.resolveRenameLocation) { - information = await asWinJsPromise(token => provider.resolveRenameLocation(this.model, this.position, token)); + res = await asWinJsPromise(token => provider.resolveRenameLocation(this.model, this.position, token)); } - if (!information) { + if (!res) { let word = this.model.getWordAtPosition(this.position); if (word) { - information = { + res = { range: new Range(this.position.lineNumber, word.startColumn, this.position.lineNumber, word.endColumn), text: word.word }; } } - return information; + return res; } - async provideRenameEdits(newName: string, i: number = 0, rejects: string[] = []): TPromise { + async provideRenameEdits(newName: string, i: number = 0, rejects: string[] = [], position: Position = this.position): TPromise { if (i >= this._provider.length) { return { @@ -134,27 +134,29 @@ class RenameController implements IEditorContribution { const position = this.editor.getPosition(); const skeleton = new RenameSkeleton(this.editor.getModel(), position); - let context = await skeleton.resolveRenameInformation(); - if (!context) { + let loc: RenameLocation; + try { + loc = await skeleton.resolveRenameLocation(); + } catch (e) { + MessageController.get(this.editor).showMessage(e, position); return undefined; } - if (context.message) { - MessageController.get(this.editor).showMessage(context.message, position); + if (!loc) { return undefined; } let selection = this.editor.getSelection(); let selectionStart = 0; - let selectionEnd = context.text.length; + let selectionEnd = loc.text.length; - if (!selection.isEmpty() && selection.startLineNumber === selection.endLineNumber) { - selectionStart = Math.max(0, selection.startColumn - context.range.startColumn); - selectionEnd = Math.min(context.range.endColumn, selection.endColumn) - context.range.startColumn; + if (!Range.isEmpty(selection) && !Range.spansMultipleLines(selection) && Range.containsRange(loc.range, selection)) { + selectionStart = Math.max(0, selection.startColumn - loc.range.startColumn); + selectionEnd = Math.min(loc.range.endColumn, selection.endColumn) - loc.range.startColumn; } this._renameInputVisible.set(true); - return this._renameInputField.getInput(Range.lift(context.range), context.text, selectionStart, selectionEnd).then(newNameOrFocusFlag => { + return this._renameInputField.getInput(loc.range, loc.text, selectionStart, selectionEnd).then(newNameOrFocusFlag => { this._renameInputVisible.reset(); if (typeof newNameOrFocusFlag === 'boolean') { @@ -169,7 +171,7 @@ class RenameController implements IEditorContribution { const edit = new BulkEdit(this.editor, null, this._textModelResolverService, this._fileService); const state = new EditorState(this.editor, CodeEditorStateFlag.Position | CodeEditorStateFlag.Value | CodeEditorStateFlag.Selection | CodeEditorStateFlag.Scroll); - const renameOperation = skeleton.provideRenameEdits(newNameOrFocusFlag).then(result => { + const renameOperation = skeleton.provideRenameEdits(newNameOrFocusFlag, 0, [], Range.lift(loc.range).getStartPosition()).then(result => { if (result.rejectReason) { if (state.validate(this.editor)) { MessageController.get(this.editor).showMessage(result.rejectReason, this.editor.getPosition()); @@ -185,7 +187,7 @@ class RenameController implements IEditorContribution { this.editor.setSelection(selection); } // alert - alert(nls.localize('aria', "Successfully renamed '{0}' to '{1}'. Summary: {2}", context.text, newNameOrFocusFlag, edit.ariaMessage())); + alert(nls.localize('aria', "Successfully renamed '{0}' to '{1}'. Summary: {2}", loc.text, newNameOrFocusFlag, edit.ariaMessage())); }); }, err => { diff --git a/src/vs/editor/contrib/rename/renameInputField.ts b/src/vs/editor/contrib/rename/renameInputField.ts index 76f4feab71a..e7f6c7df33a 100644 --- a/src/vs/editor/contrib/rename/renameInputField.ts +++ b/src/vs/editor/contrib/rename/renameInputField.ts @@ -9,7 +9,7 @@ import 'vs/css!./renameInputField'; import { localize } from 'vs/nls'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { TPromise } from 'vs/base/common/winjs.base'; -import { Range } from 'vs/editor/common/core/range'; +import { Range, IRange } from 'vs/editor/common/core/range'; import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from 'vs/editor/browser/editorBrowser'; import { IThemeService, ITheme } from 'vs/platform/theme/common/themeService'; import { inputBackground, inputBorder, inputForeground, widgetShadow } from 'vs/platform/theme/common/colorRegistry'; @@ -123,7 +123,7 @@ export default class RenameInputField implements IContentWidget, IDisposable { } } - public getInput(where: Range, value: string, selectionStart: number, selectionEnd: number): TPromise { + public getInput(where: IRange, value: string, selectionStart: number, selectionEnd: number): TPromise { this._position = new Position(where.startLineNumber, where.startColumn); this._inputField.value = value; @@ -192,7 +192,7 @@ export default class RenameInputField implements IContentWidget, IDisposable { this._inputField.setSelectionRange( parseInt(this._inputField.getAttribute('selectionStart')), parseInt(this._inputField.getAttribute('selectionEnd'))); - }, 25); + }, 100); } private _hide(): void { diff --git a/src/vs/editor/contrib/snippet/snippetController2.ts b/src/vs/editor/contrib/snippet/snippetController2.ts index 9ec45c47c8f..83abd441a26 100644 --- a/src/vs/editor/contrib/snippet/snippetController2.ts +++ b/src/vs/editor/contrib/snippet/snippetController2.ts @@ -14,6 +14,7 @@ import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { showSimpleSuggestions } from 'vs/editor/contrib/suggest/suggest'; import { ISuggestion } from 'vs/editor/common/modes'; import { Selection } from 'vs/editor/common/core/selection'; +import { Range } from 'vs/editor/common/core/range'; import { Choice } from 'vs/editor/contrib/snippet/snippetParser'; import { repeat } from 'vs/base/common/strings'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; @@ -203,6 +204,13 @@ export class SnippetController2 implements IEditorContribution { this._session.next(); this._updateState(); } + + getSessionEnclosingRange(): Range { + if (this._session) { + return this._session.getEnclosingRange(); + } + return undefined; + } } diff --git a/src/vs/editor/contrib/snippet/snippetSession.ts b/src/vs/editor/contrib/snippet/snippetSession.ts index 218e5ec9fd5..31be6b2ba16 100644 --- a/src/vs/editor/contrib/snippet/snippetSession.ts +++ b/src/vs/editor/contrib/snippet/snippetSession.ts @@ -50,7 +50,9 @@ export class OneSnippet { dispose(): void { if (this._placeholderDecorations) { - this._editor.changeDecorations(accessor => this._placeholderDecorations.forEach(handle => accessor.removeDecoration(handle))); + let toRemove: string[] = []; + this._placeholderDecorations.forEach(handle => toRemove.push(handle)); + this._editor.deltaDecorations(toRemove, []); } this._placeholderGroups.length = 0; } @@ -224,6 +226,20 @@ export class OneSnippet { this._placeholderGroups = groupBy(this._snippet.placeholders, Placeholder.compareByIndex); }); } + + public getEnclosingRange(): Range { + let result: Range; + const model = this._editor.getModel(); + this._placeholderDecorations.forEach((decorationId) => { + const placeholderRange = model.getDecorationRange(decorationId); + if (!result) { + result = placeholderRange; + } else { + result = result.plusRange(placeholderRange); + } + }); + return result; + } } export class SnippetSession { @@ -365,13 +381,15 @@ export class SnippetSession { const { edits, snippets } = SnippetSession.createEditsAndSnippets(this._editor, this._template, this._overwriteBefore, this._overwriteAfter, false); this._snippets = snippets; - this._editor.setSelections(model.pushEditOperations(this._editor.getSelections(), edits, undoEdits => { + const selections = model.pushEditOperations(this._editor.getSelections(), edits, undoEdits => { if (this._snippets[0].hasPlaceholder) { return this._move(true); } else { return undoEdits.map(edit => Selection.fromPositions(edit.range.getEndPosition())); } - })); + }); + this._editor.setSelections(selections); + this._editor.revealRange(selections[0]); } merge(template: string, overwriteBefore: number = 0, overwriteAfter: number = 0): void { @@ -506,4 +524,17 @@ export class SnippetSession { // have any left, we don't have a selection anymore return allPossibleSelections.size > 0; } + + public getEnclosingRange(): Range { + let result: Range; + for (const snippet of this._snippets) { + const snippetRange = snippet.getEnclosingRange(); + if (!result) { + result = snippetRange; + } else { + result = result.plusRange(snippetRange); + } + } + return result; + } } diff --git a/src/vs/editor/contrib/suggest/suggest.ts b/src/vs/editor/contrib/suggest/suggest.ts index fcc39c22299..8136074bc0f 100644 --- a/src/vs/editor/contrib/suggest/suggest.ts +++ b/src/vs/editor/contrib/suggest/suggest.ts @@ -210,13 +210,20 @@ registerDefaultLanguageCommand('_executeCompletionItemProvider', (model, positio suggestions: [] }; + let resolving: Thenable[] = []; + let maxItemsToResolve = args['maxItemsToResolve'] || 0; + return provideSuggestionItems(model, position).then(items => { - - for (const { container, suggestion } of items) { - result.incomplete = result.incomplete || container.incomplete; - result.suggestions.push(suggestion); + for (const item of items) { + if (resolving.length < maxItemsToResolve) { + resolving.push(item.resolve()); + } + result.incomplete = result.incomplete || item.container.incomplete; + result.suggestions.push(item.suggestion); } - + }).then(() => { + return TPromise.join(resolving); + }).then(() => { return result; }); }); diff --git a/src/vs/editor/contrib/suggest/suggestMemory.ts b/src/vs/editor/contrib/suggest/suggestMemory.ts index 6351ea04471..29c6afc906d 100644 --- a/src/vs/editor/contrib/suggest/suggestMemory.ts +++ b/src/vs/editor/contrib/suggest/suggestMemory.ts @@ -58,7 +58,7 @@ export class LRUMemory extends Memory { this._cache.set(key, { touch: this._seq++, type: item.suggestion.type, - insertText: undefined + insertText: item.suggestion.insertText }); } @@ -66,18 +66,24 @@ export class LRUMemory extends Memory { // in order of completions, select the first // that has been used in the past let { word } = model.getWordUntilPosition(pos); + if (word.length !== 0) { + return 0; + } + + let lineSuffix = model.getLineContent(pos.lineNumber).substr(pos.column - 10, pos.column - 1); + if (/\s$/.test(lineSuffix)) { + return 0; + } let res = 0; let seq = -1; - if (word.length === 0) { - for (let i = 0; i < items.length; i++) { - const { suggestion } = items[i]; - const key = `${model.getLanguageIdentifier().language}/${suggestion.label}`; - const item = this._cache.get(key); - if (item && item.touch > seq && item.type === suggestion.type) { - seq = item.touch; - res = i; - } + for (let i = 0; i < items.length; i++) { + const { suggestion } = items[i]; + const key = `${model.getLanguageIdentifier().language}/${suggestion.label}`; + const item = this._cache.get(key); + if (item && item.touch > seq && item.type === suggestion.type && item.insertText === suggestion.insertText) { + seq = item.touch; + res = i; } } return res; diff --git a/src/vs/editor/contrib/suggest/suggestWidget.ts b/src/vs/editor/contrib/suggest/suggestWidget.ts index 4b904459a48..2551ccfaee2 100644 --- a/src/vs/editor/contrib/suggest/suggestWidget.ts +++ b/src/vs/editor/contrib/suggest/suggestWidget.ts @@ -176,7 +176,6 @@ class Renderer implements IRenderer { } disposeTemplate(templateData: ISuggestionTemplateData): void { - templateData.highlightedLabel.dispose(); templateData.disposables = dispose(templateData.disposables); } } @@ -439,7 +438,8 @@ export class SuggestWidget implements IContentWidget, IDelegate this.list = new List(this.listElement, this, [renderer], { useShadows: false, selectOnMouseDown: true, - focusOnMouseDown: false + focusOnMouseDown: false, + openController: { shouldOpen: () => false } }); this.toDispose = [ diff --git a/src/vs/editor/contrib/suggest/test/suggestMemory.test.ts b/src/vs/editor/contrib/suggest/test/suggestMemory.test.ts index 9d69016057f..4025fcd221e 100644 --- a/src/vs/editor/contrib/suggest/test/suggestMemory.test.ts +++ b/src/vs/editor/contrib/suggest/test/suggestMemory.test.ts @@ -21,7 +21,7 @@ suite('SuggestMemories', function () { setup(function () { pos = { lineNumber: 1, column: 1 }; - buffer = TextModel.createFromString('This is some text'); + buffer = TextModel.createFromString('This is some text.\nthis.\nfoo: ,'); items = [ createSuggestItem('foo', 0), createSuggestItem('bar', 0) @@ -39,7 +39,9 @@ suite('SuggestMemories', function () { mem.memorize(buffer, pos, null); }); - test('ShyMemories', function () { + test('LRUMemory', function () { + + pos = { lineNumber: 2, column: 6 }; const mem = new LRUMemory(); mem.memorize(buffer, pos, items[1]); @@ -59,7 +61,19 @@ suite('SuggestMemories', function () { createSuggestItem('new1', 0), createSuggestItem('new2', 0) ]), 0); + }); + test('intellisense is not showing top options first #43429', function () { + // ensure we don't memorize for whitespace prefixes + + pos = { lineNumber: 2, column: 6 }; + const mem = new LRUMemory(); + + mem.memorize(buffer, pos, items[1]); + assert.equal(mem.select(buffer, pos, items), 1); + + assert.equal(mem.select(buffer, { lineNumber: 3, column: 5 }, items), 0); // foo: |, + assert.equal(mem.select(buffer, { lineNumber: 3, column: 6 }, items), 1); // foo: ,| }); test('PrefixMemory', function () { diff --git a/src/vs/editor/contrib/suggest/test/suggestModel.test.ts b/src/vs/editor/contrib/suggest/test/suggestModel.test.ts index efaf16b11a9..c6e4aa184b2 100644 --- a/src/vs/editor/contrib/suggest/test/suggestModel.test.ts +++ b/src/vs/editor/contrib/suggest/test/suggestModel.test.ts @@ -31,17 +31,21 @@ import { MockMode } from 'vs/editor/test/common/mocks/mockMode'; import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; import { TokenizationResult2 } from 'vs/editor/common/core/token'; import { NULL_STATE } from 'vs/editor/common/modes/nullMode'; +import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; +import { INotificationService } from 'vs/platform/notification/common/notification'; function createMockEditor(model: TextModel): TestCodeEditor { const contextKeyService = new MockContextKeyService(); const telemetryService = NullTelemetryService; + const notificationService = new TestNotificationService(); const instantiationService = new InstantiationService(new ServiceCollection( [IContextKeyService, contextKeyService], [ITelemetryService, telemetryService], - [IStorageService, NullStorageService] + [IStorageService, NullStorageService], + [INotificationService, TestNotificationService] )); - const editor = new TestCodeEditor(new MockScopeLocation(), {}, false, instantiationService, contextKeyService); + const editor = new TestCodeEditor(new MockScopeLocation(), {}, false, instantiationService, contextKeyService, notificationService); editor.setModel(model); return editor; } diff --git a/src/vs/editor/contrib/wordHighlighter/wordHighlighter.ts b/src/vs/editor/contrib/wordHighlighter/wordHighlighter.ts index 76dc078f02b..dd664d73fb7 100644 --- a/src/vs/editor/contrib/wordHighlighter/wordHighlighter.ts +++ b/src/vs/editor/contrib/wordHighlighter/wordHighlighter.ts @@ -504,30 +504,34 @@ registerEditorAction(NextWordHighlightAction); registerEditorAction(PrevWordHighlightAction); registerThemingParticipant((theme, collector) => { - let selectionHighlight = theme.getColor(editorSelectionHighlight); + const selectionHighlight = theme.getColor(editorSelectionHighlight); if (selectionHighlight) { collector.addRule(`.monaco-editor .focused .selectionHighlight { background-color: ${selectionHighlight}; }`); collector.addRule(`.monaco-editor .selectionHighlight { background-color: ${selectionHighlight.transparent(0.5)}; }`); } - let wordHighlight = theme.getColor(editorWordHighlight); + + const wordHighlight = theme.getColor(editorWordHighlight); if (wordHighlight) { collector.addRule(`.monaco-editor .wordHighlight { background-color: ${wordHighlight}; }`); } - let wordHighlightStrong = theme.getColor(editorWordHighlightStrong); + + const wordHighlightStrong = theme.getColor(editorWordHighlightStrong); if (wordHighlightStrong) { collector.addRule(`.monaco-editor .wordHighlightStrong { background-color: ${wordHighlightStrong}; }`); } - let selectionHighlightBorder = theme.getColor(editorSelectionHighlightBorder); + + const selectionHighlightBorder = theme.getColor(editorSelectionHighlightBorder); if (selectionHighlightBorder) { - collector.addRule(`.monaco-editor .selectionHighlight { border: 1px dotted ${selectionHighlightBorder}; box-sizing: border-box; }`); - } - let wordHighlightBorder = theme.getColor(editorWordHighlightBorder); - if (wordHighlightBorder) { - collector.addRule(`.monaco-editor .wordHighlight { border: 1px dashed ${wordHighlightBorder}; box-sizing: border-box; }`); - } - let wordHighlightStrongBorder = theme.getColor(editorWordHighlightStrongBorder); - if (wordHighlightStrongBorder) { - collector.addRule(`.monaco-editor .wordHighlightStrong { border: 1px dashed ${wordHighlightStrongBorder}; box-sizing: border-box; }`); + collector.addRule(`.monaco-editor .selectionHighlight { border: 1px ${theme.type === 'hc' ? 'dotted' : 'solid'} ${selectionHighlightBorder}; box-sizing: border-box; }`); } + const wordHighlightBorder = theme.getColor(editorWordHighlightBorder); + if (wordHighlightBorder) { + collector.addRule(`.monaco-editor .wordHighlight { border: 1px ${theme.type === 'hc' ? 'dashed' : 'solid'} ${wordHighlightBorder}; box-sizing: border-box; }`); + } + + const wordHighlightStrongBorder = theme.getColor(editorWordHighlightStrongBorder); + if (wordHighlightStrongBorder) { + collector.addRule(`.monaco-editor .wordHighlightStrong { border: 1px ${theme.type === 'hc' ? 'dashed' : 'solid'} ${wordHighlightStrongBorder}; box-sizing: border-box; }`); + } }); diff --git a/src/vs/editor/contrib/wordOperations/test/wordOperations.test.ts b/src/vs/editor/contrib/wordOperations/test/wordOperations.test.ts index aaea8d65c8c..bd34ee01bda 100644 --- a/src/vs/editor/contrib/wordOperations/test/wordOperations.test.ts +++ b/src/vs/editor/contrib/wordOperations/test/wordOperations.test.ts @@ -141,7 +141,6 @@ suite('WordOperations', () => { moveWordLeft(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a+= 3 +5-3 + '.length + 1, '002'); moveWordLeft(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a+= 3 +5-3 '.length + 1, '003'); moveWordLeft(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a+= 3 +5-'.length + 1, '004'); - moveWordLeft(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a+= 3 +5'.length + 1, '005'); moveWordLeft(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a+= 3 +'.length + 1, '006'); moveWordLeft(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a+= 3 '.length + 1, '007'); moveWordLeft(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a+= '.length + 1, '008'); @@ -165,7 +164,6 @@ suite('WordOperations', () => { moveWordStartLeft(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a+= 3 +5-3 + '.length + 1, '002'); moveWordStartLeft(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a+= 3 +5-3 '.length + 1, '003'); moveWordStartLeft(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a+= 3 +5-'.length + 1, '004'); - moveWordStartLeft(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a+= 3 +5'.length + 1, '005'); moveWordStartLeft(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a+= 3 +'.length + 1, '006'); moveWordStartLeft(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a+= 3 '.length + 1, '007'); moveWordStartLeft(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a+= '.length + 1, '008'); @@ -267,9 +265,7 @@ suite('WordOperations', () => { moveWordRight(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a'.length + 1, '007'); moveWordRight(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a+='.length + 1, '008'); moveWordRight(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a+= 3'.length + 1, '009'); - moveWordRight(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a+= 3 +'.length + 1, '010'); moveWordRight(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a+= 3 +5'.length + 1, '011'); - moveWordRight(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a+= 3 +5-'.length + 1, '012'); moveWordRight(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a+= 3 +5-3'.length + 1, '013'); moveWordRight(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a+= 3 +5-3 +'.length + 1, '014'); moveWordRight(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a+= 3 +5-3 + 7'.length + 1, '015'); @@ -279,6 +275,31 @@ suite('WordOperations', () => { }); }); + test('issue #41199: moveWordRight', () => { + withTestCodeEditor([ + 'console.log(err)' + ], {}, (editor, _) => { + editor.setPosition(new Position(1, 1)); + + moveWordRight(editor); assert.equal(editor.getPosition().column, 'console'.length + 1, '001'); + moveWordRight(editor); assert.equal(editor.getPosition().column, 'console.log'.length + 1, '002'); + moveWordRight(editor); assert.equal(editor.getPosition().column, 'console.log(err'.length + 1, '003'); + moveWordRight(editor); assert.equal(editor.getPosition().column, 'console.log(err)'.length + 1, '004'); + }); + }); + + test('issue #48046: Word selection doesn\'t work as usual', () => { + withTestCodeEditor([ + 'deep.object.property' + ], {}, (editor, _) => { + editor.setPosition(new Position(1, 21)); + + moveWordLeft(editor); assert.equal(editor.getPosition().column, 'deep.object.'.length + 1, '001'); + moveWordLeft(editor); assert.equal(editor.getPosition().column, 'deep.'.length + 1, '002'); + moveWordLeft(editor); assert.equal(editor.getPosition().column, ''.length + 1, '003'); + }); + }); + test('moveWordEndRight', () => { withTestCodeEditor([ ' /* Just some more text a+= 3 +5-3 + 7 */ ' @@ -293,9 +314,7 @@ suite('WordOperations', () => { moveWordEndRight(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a'.length + 1, '007'); moveWordEndRight(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a+='.length + 1, '008'); moveWordEndRight(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a+= 3'.length + 1, '009'); - moveWordEndRight(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a+= 3 +'.length + 1, '010'); moveWordEndRight(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a+= 3 +5'.length + 1, '011'); - moveWordEndRight(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a+= 3 +5-'.length + 1, '012'); moveWordEndRight(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a+= 3 +5-3'.length + 1, '013'); moveWordEndRight(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a+= 3 +5-3 +'.length + 1, '014'); moveWordEndRight(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a+= 3 +5-3 + 7'.length + 1, '015'); diff --git a/src/vs/editor/contrib/zoneWidget/zoneWidget.ts b/src/vs/editor/contrib/zoneWidget/zoneWidget.ts index ed08e1974ed..784aac39e55 100644 --- a/src/vs/editor/contrib/zoneWidget/zoneWidget.ts +++ b/src/vs/editor/contrib/zoneWidget/zoneWidget.ts @@ -210,6 +210,7 @@ export abstract class ZoneWidget implements IHorizontalSashLayoutProvider { } this.editor.deltaDecorations(this._positionMarkerId, []); + this._positionMarkerId = []; } public create(): void { @@ -280,10 +281,14 @@ export abstract class ZoneWidget implements IHorizontalSashLayoutProvider { public get position(): Position { const [id] = this._positionMarkerId; - if (id) { - return this.editor.getModel().getDecorationRange(id).getStartPosition(); + if (!id) { + return undefined; } - return undefined; + const range = this.editor.getModel().getDecorationRange(id); + if (!range) { + return undefined; + } + return range.getStartPosition(); } protected _isShowing: boolean = false; diff --git a/src/vs/editor/editor.all.ts b/src/vs/editor/editor.all.ts index d4f93d0a876..07cbf332285 100644 --- a/src/vs/editor/editor.all.ts +++ b/src/vs/editor/editor.all.ts @@ -32,7 +32,7 @@ import 'vs/editor/contrib/linesOperations/linesOperations'; import 'vs/editor/contrib/links/links'; import 'vs/editor/contrib/multicursor/multicursor'; import 'vs/editor/contrib/parameterHints/parameterHints'; -import 'vs/editor/contrib/quickFix/quickFixCommands'; +import 'vs/editor/contrib/codeAction/codeActionContributions'; import 'vs/editor/contrib/referenceSearch/referenceSearch'; import 'vs/editor/contrib/rename/rename'; import 'vs/editor/contrib/smartSelect/smartSelect'; diff --git a/src/vs/editor/editor.main.ts b/src/vs/editor/editor.main.ts index 3f2c56b5d16..08fd5c92326 100644 --- a/src/vs/editor/editor.main.ts +++ b/src/vs/editor/editor.main.ts @@ -13,5 +13,6 @@ import 'vs/editor/standalone/browser/quickOpen/quickOutline'; import 'vs/editor/standalone/browser/quickOpen/gotoLine'; import 'vs/editor/standalone/browser/quickOpen/quickCommand'; import 'vs/editor/standalone/browser/toggleHighContrast/toggleHighContrast'; +import 'vs/editor/standalone/browser/referenceSearch/standaloneReferenceSearch'; export * from 'vs/editor/editor.api'; diff --git a/src/vs/editor/standalone/browser/colorizer.ts b/src/vs/editor/standalone/browser/colorizer.ts index 83603e32539..61e3b448392 100644 --- a/src/vs/editor/standalone/browser/colorizer.ts +++ b/src/vs/editor/standalone/browser/colorizer.ts @@ -13,6 +13,7 @@ import { renderViewLine2 as renderViewLine, RenderLineInput } from 'vs/editor/co import { LineTokens, IViewLineTokens } from 'vs/editor/common/core/lineTokens'; import * as strings from 'vs/base/common/strings'; import { IStandaloneThemeService } from 'vs/editor/standalone/common/standaloneThemeService'; +import { ViewLineRenderingData } from 'vs/editor/common/viewModel/viewModel'; export interface IColorizerOptions { tabSize?: number; @@ -93,11 +94,14 @@ export class Colorizer { }); } - public static colorizeLine(line: string, mightContainRTL: boolean, tokens: IViewLineTokens, tabSize: number = 4): string { + public static colorizeLine(line: string, mightContainNonBasicASCII: boolean, mightContainRTL: boolean, tokens: IViewLineTokens, tabSize: number = 4): string { + const isBasicASCII = ViewLineRenderingData.isBasicASCII(line, mightContainNonBasicASCII); + const containsRTL = ViewLineRenderingData.containsRTL(line, isBasicASCII, mightContainRTL); let renderResult = renderViewLine(new RenderLineInput( false, line, - mightContainRTL, + isBasicASCII, + containsRTL, 0, tokens, [], @@ -116,7 +120,7 @@ export class Colorizer { model.forceTokenization(lineNumber); let tokens = model.getLineTokens(lineNumber); let inflatedTokens = tokens.inflate(); - return this.colorizeLine(content, model.mightContainRTL(), inflatedTokens, tabSize); + return this.colorizeLine(content, model.mightContainNonBasicASCII(), model.mightContainRTL(), inflatedTokens, tabSize); } } @@ -143,10 +147,13 @@ function _fakeColorize(lines: string[], tabSize: number): string { tokens[0] = line.length; const lineTokens = new LineTokens(tokens, line); + const isBasicASCII = ViewLineRenderingData.isBasicASCII(line, /* check for basic ASCII */true); + const containsRTL = ViewLineRenderingData.containsRTL(line, isBasicASCII, /* check for RTL */true); let renderResult = renderViewLine(new RenderLineInput( false, line, - false, + isBasicASCII, + containsRTL, 0, lineTokens, [], @@ -174,10 +181,13 @@ function _actualColorize(lines: string[], tabSize: number, tokenizationSupport: let tokenizeResult = tokenizationSupport.tokenize2(line, state, 0); LineTokens.convertToEndOffset(tokenizeResult.tokens, line.length); let lineTokens = new LineTokens(tokenizeResult.tokens, line); + const isBasicASCII = ViewLineRenderingData.isBasicASCII(line, /* check for basic ASCII */true); + const containsRTL = ViewLineRenderingData.containsRTL(line, isBasicASCII, /* check for RTL */true); let renderResult = renderViewLine(new RenderLineInput( false, line, - true/* check for RTL */, + isBasicASCII, + containsRTL, 0, lineTokens.inflate(), [], diff --git a/src/vs/editor/standalone/browser/quickOpen/editorQuickOpen.ts b/src/vs/editor/standalone/browser/quickOpen/editorQuickOpen.ts index 8d1bce53f7b..8bd223b6a2f 100644 --- a/src/vs/editor/standalone/browser/quickOpen/editorQuickOpen.ts +++ b/src/vs/editor/standalone/browser/quickOpen/editorQuickOpen.ts @@ -14,7 +14,7 @@ import { registerEditorContribution, IActionOptions, EditorAction } from 'vs/edi import { IThemeService } from 'vs/platform/theme/common/themeService'; import { Range } from 'vs/editor/common/core/range'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; -import { IModelDecorationsChangeAccessor, IModelDeltaDecoration } from 'vs/editor/common/model'; +import { IModelDeltaDecoration } from 'vs/editor/common/model'; export interface IQuickOpenControllerOpts { inputAriaLabel: string; @@ -100,31 +100,27 @@ export class QuickOpenController implements editorCommon.IEditorContribution, ID }); public decorateLine(range: Range, editor: ICodeEditor): void { - editor.changeDecorations((changeAccessor: IModelDecorationsChangeAccessor) => { - const oldDecorations: string[] = []; - if (this.rangeHighlightDecorationId) { - oldDecorations.push(this.rangeHighlightDecorationId); - this.rangeHighlightDecorationId = null; + const oldDecorations: string[] = []; + if (this.rangeHighlightDecorationId) { + oldDecorations.push(this.rangeHighlightDecorationId); + this.rangeHighlightDecorationId = null; + } + + const newDecorations: IModelDeltaDecoration[] = [ + { + range: range, + options: QuickOpenController._RANGE_HIGHLIGHT_DECORATION } + ]; - const newDecorations: IModelDeltaDecoration[] = [ - { - range: range, - options: QuickOpenController._RANGE_HIGHLIGHT_DECORATION - } - ]; - - const decorations = changeAccessor.deltaDecorations(oldDecorations, newDecorations); - this.rangeHighlightDecorationId = decorations[0]; - }); + const decorations = editor.deltaDecorations(oldDecorations, newDecorations); + this.rangeHighlightDecorationId = decorations[0]; } public clearDecorations(): void { if (this.rangeHighlightDecorationId) { - this.editor.changeDecorations((changeAccessor: IModelDecorationsChangeAccessor) => { - changeAccessor.deltaDecorations([this.rangeHighlightDecorationId], []); - this.rangeHighlightDecorationId = null; - }); + this.editor.deltaDecorations([this.rangeHighlightDecorationId], []); + this.rangeHighlightDecorationId = null; } } } diff --git a/src/vs/editor/standalone/browser/quickOpen/quickOpenEditorWidget.ts b/src/vs/editor/standalone/browser/quickOpen/quickOpenEditorWidget.ts index c8b3d511cd3..7eade63fcd7 100644 --- a/src/vs/editor/standalone/browser/quickOpen/quickOpenEditorWidget.ts +++ b/src/vs/editor/standalone/browser/quickOpen/quickOpenEditorWidget.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import { $, Dimension } from 'vs/base/browser/builder'; import { QuickOpenModel } from 'vs/base/parts/quickopen/browser/quickOpenModel'; import { QuickOpenWidget } from 'vs/base/parts/quickopen/browser/quickOpenWidget'; import { IAutoFocus } from 'vs/base/parts/quickopen/common/quickOpen'; @@ -13,6 +12,7 @@ import { attachQuickOpenStyler } from 'vs/platform/theme/common/styler'; import { IDisposable } from 'vs/base/common/lifecycle'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { foreground } from 'vs/platform/theme/common/colorRegistry'; +import { Dimension } from 'vs/base/browser/dom'; export interface IQuickOpenEditorWidgetOptions { inputAriaLabel: string; @@ -37,7 +37,7 @@ export class QuickOpenEditorWidget implements IOverlayWidget { } private create(onOk: () => void, onCancel: () => void, onType: (value: string) => void, configuration: IQuickOpenEditorWidgetOptions): void { - this.domNode = $().div().getHTMLElement(); + this.domNode = document.createElement('div'); this.quickOpenWidget = new QuickOpenWidget( this.domNode, diff --git a/src/vs/editor/standalone/browser/quickOpen/quickOutline.ts b/src/vs/editor/standalone/browser/quickOpen/quickOutline.ts index a4923404f39..a09d76a25d5 100644 --- a/src/vs/editor/standalone/browser/quickOpen/quickOutline.ts +++ b/src/vs/editor/standalone/browser/quickOpen/quickOutline.ts @@ -15,7 +15,7 @@ import { IContext, IHighlight, QuickOpenEntryGroup, QuickOpenModel } from 'vs/ba import { IAutoFocus, Mode } from 'vs/base/parts/quickopen/common/quickOpen'; import { ScrollType } from 'vs/editor/common/editorCommon'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; -import { SymbolInformation, DocumentSymbolProviderRegistry, symbolKindToCssClass, IOutline } from 'vs/editor/common/modes'; +import { SymbolInformation, DocumentSymbolProviderRegistry, symbolKindToCssClass, IOutline, Location } from 'vs/editor/common/modes'; import { BaseEditorQuickOpenAction, IDecorator } from './editorQuickOpen'; import { getDocumentSymbols } from 'vs/editor/contrib/quickOpen/quickOpen'; import { registerEditorAction, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; @@ -25,7 +25,7 @@ import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; let SCOPE_PREFIX = ':'; -class SymbolEntry extends QuickOpenEntryGroup { +export class SymbolEntry extends QuickOpenEntryGroup { private name: string; private type: string; private description: string; @@ -167,6 +167,10 @@ export class QuickOutlineAction extends BaseEditorQuickOpenAction { }); } + private symbolEntry(name: string, type: string, description: string, location: Location, highlights: IHighlight[], editor: ICodeEditor, decorator: IDecorator): SymbolEntry { + return new SymbolEntry(name, type, description, Range.lift(location.range), highlights, editor, decorator); + } + private toQuickOpenEntries(editor: ICodeEditor, flattened: SymbolInformation[], searchValue: string): SymbolEntry[] { const controller = this.getController(editor); @@ -193,7 +197,7 @@ export class QuickOutlineAction extends BaseEditorQuickOpenAction { } // Add - results.push(new SymbolEntry(label, symbolKindToCssClass(element.kind), description, Range.lift(element.location.range), highlights, editor, controller)); + results.push(this.symbolEntry(label, symbolKindToCssClass(element.kind), description, element.location, highlights, editor, controller)); } } diff --git a/src/vs/editor/standalone/browser/referenceSearch/standaloneReferenceSearch.ts b/src/vs/editor/standalone/browser/referenceSearch/standaloneReferenceSearch.ts new file mode 100644 index 00000000000..4886fd86e1b --- /dev/null +++ b/src/vs/editor/standalone/browser/referenceSearch/standaloneReferenceSearch.ts @@ -0,0 +1,53 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { IEditorService } from 'vs/platform/editor/common/editor'; +import { IInstantiationService, optional } from 'vs/platform/instantiation/common/instantiation'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { IStorageService } from 'vs/platform/storage/common/storage'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; +import { ITextModelService } from 'vs/editor/common/services/resolverService'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { ReferencesController } from 'vs/editor/contrib/referenceSearch/referencesController'; + +export class StandaloneReferencesController extends ReferencesController { + + public constructor( + editor: ICodeEditor, + @IContextKeyService contextKeyService: IContextKeyService, + @IEditorService editorService: IEditorService, + @ITextModelService textModelResolverService: ITextModelService, + @INotificationService notificationService: INotificationService, + @IInstantiationService instantiationService: IInstantiationService, + @IWorkspaceContextService contextService: IWorkspaceContextService, + @IStorageService storageService: IStorageService, + @IThemeService themeService: IThemeService, + @IConfigurationService configurationService: IConfigurationService, + @optional(IEnvironmentService) environmentService: IEnvironmentService + ) { + super( + true, + editor, + contextKeyService, + editorService, + textModelResolverService, + notificationService, + instantiationService, + contextService, + storageService, + themeService, + configurationService, + environmentService + ); + } +} + +registerEditorContribution(StandaloneReferencesController); diff --git a/src/vs/editor/standalone/browser/simpleServices.ts b/src/vs/editor/standalone/browser/simpleServices.ts index 3a45cc198a9..a09429f1137 100644 --- a/src/vs/editor/standalone/browser/simpleServices.ts +++ b/src/vs/editor/standalone/browser/simpleServices.ts @@ -38,7 +38,7 @@ import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKe import { OS } from 'vs/base/common/platform'; import { IRange } from 'vs/editor/common/core/range'; import { ITextModel } from 'vs/editor/common/model'; -import { INotificationService, INotification, INotificationHandle, NoOpNotification, PromptOption } from 'vs/platform/notification/common/notification'; +import { INotificationService, INotification, INotificationHandle, NoOpNotification, IPromptChoice } from 'vs/platform/notification/common/notification'; import { IConfirmation, IConfirmationResult, IDialogService, IDialogOptions } from 'vs/platform/dialogs/common/dialogs'; import { IPosition, Position as Pos } from 'vs/editor/common/core/position'; @@ -297,8 +297,8 @@ export class SimpleNotificationService implements INotificationService { return SimpleNotificationService.NO_OP; } - public prompt(severity: Severity, message: string, choices: PromptOption[]): TPromise { - return TPromise.as(0); + public prompt(severity: Severity, message: string, choices: IPromptChoice[], onCancel?: () => void): INotificationHandle { + return SimpleNotificationService.NO_OP; } } diff --git a/src/vs/editor/standalone/browser/standaloneCodeEditor.ts b/src/vs/editor/standalone/browser/standaloneCodeEditor.ts index 14e25fa01f9..53a9a83ed47 100644 --- a/src/vs/editor/standalone/browser/standaloneCodeEditor.ts +++ b/src/vs/editor/standalone/browser/standaloneCodeEditor.ts @@ -164,7 +164,8 @@ export class StandaloneCodeEditor extends CodeEditor implements IStandaloneCodeE @ICommandService commandService: ICommandService, @IContextKeyService contextKeyService: IContextKeyService, @IKeybindingService keybindingService: IKeybindingService, - @IThemeService themeService: IThemeService + @IThemeService themeService: IThemeService, + @INotificationService notificationService: INotificationService ) { options = options || {}; options.ariaLabel = options.ariaLabel || nls.localize('editorViewAccessibleLabel', "Editor content"); @@ -173,7 +174,7 @@ export class StandaloneCodeEditor extends CodeEditor implements IStandaloneCodeE ? nls.localize('accessibilityHelpMessageIE', "Press Ctrl+F1 for Accessibility Options.") : nls.localize('accessibilityHelpMessage', "Press Alt+F1 for Accessibility Options.") ); - super(domElement, options, instantiationService, codeEditorService, commandService, contextKeyService, themeService); + super(domElement, options, instantiationService, codeEditorService, commandService, contextKeyService, themeService, notificationService); if (keybindingService instanceof StandaloneKeybindingService) { this._standaloneKeybindingService = keybindingService; @@ -295,7 +296,8 @@ export class StandaloneEditor extends StandaloneCodeEditor implements IStandalon @IContextKeyService contextKeyService: IContextKeyService, @IKeybindingService keybindingService: IKeybindingService, @IContextViewService contextViewService: IContextViewService, - @IStandaloneThemeService themeService: IStandaloneThemeService + @IStandaloneThemeService themeService: IStandaloneThemeService, + @INotificationService notificationService: INotificationService, ) { options = options || {}; if (typeof options.theme === 'string') { @@ -303,7 +305,7 @@ export class StandaloneEditor extends StandaloneCodeEditor implements IStandalon } let model: ITextModel = options.model; delete options.model; - super(domElement, options, instantiationService, codeEditorService, commandService, contextKeyService, keybindingService, themeService); + super(domElement, options, instantiationService, codeEditorService, commandService, contextKeyService, keybindingService, themeService, notificationService); this._contextViewService = contextViewService; this._register(toDispose); diff --git a/src/vs/editor/standalone/browser/standaloneEditor.ts b/src/vs/editor/standalone/browser/standaloneEditor.ts index 53493117eb5..f6fbf6ba71d 100644 --- a/src/vs/editor/standalone/browser/standaloneEditor.ts +++ b/src/vs/editor/standalone/browser/standaloneEditor.ts @@ -89,7 +89,8 @@ export function create(domElement: HTMLElement, options?: IEditorConstructionOpt services.get(IContextKeyService), services.get(IKeybindingService), services.get(IContextViewService), - services.get(IStandaloneThemeService) + services.get(IStandaloneThemeService), + services.get(INotificationService) ); }); } diff --git a/src/vs/editor/standalone/browser/standaloneLanguages.ts b/src/vs/editor/standalone/browser/standaloneLanguages.ts index c0eec9275ac..eaa070aeda8 100644 --- a/src/vs/editor/standalone/browser/standaloneLanguages.ts +++ b/src/vs/editor/standalone/browser/standaloneLanguages.ts @@ -391,12 +391,11 @@ export function registerColorProvider(languageId: string, provider: modes.Docume } /** - * Register a folding provider + * Register a folding range provider */ -/*export function registerFoldingProvider(languageId: string, provider: modes.FoldingProvider): IDisposable { - return modes.FoldingProviderRegistry.register(languageId, provider); -}*/ - +export function registerFoldingRangeProvider(languageId: string, provider: modes.FoldingRangeProvider): IDisposable { + return modes.FoldingRangeProviderRegistry.register(languageId, provider); +} /** * Contains additional diagnostic information about the context in which @@ -787,12 +786,14 @@ export function createMonacoLanguagesAPI(): typeof monaco.languages { registerOnTypeFormattingEditProvider: registerOnTypeFormattingEditProvider, registerLinkProvider: registerLinkProvider, registerColorProvider: registerColorProvider, + registerFoldingRangeProvider: registerFoldingRangeProvider, // enums DocumentHighlightKind: modes.DocumentHighlightKind, CompletionItemKind: CompletionItemKind, SymbolKind: modes.SymbolKind, IndentAction: IndentAction, - SuggestTriggerKind: modes.SuggestTriggerKind + SuggestTriggerKind: modes.SuggestTriggerKind, + FoldingRangeKind: modes.FoldingRangeKind }; } diff --git a/src/vs/editor/standalone/browser/standaloneServices.ts b/src/vs/editor/standalone/browser/standaloneServices.ts index c84ca2cf87d..b1086e42833 100644 --- a/src/vs/editor/standalone/browser/standaloneServices.ts +++ b/src/vs/editor/standalone/browser/standaloneServices.ts @@ -42,6 +42,7 @@ import { StandaloneThemeServiceImpl } from 'vs/editor/standalone/browser/standal import { ILogService, NullLogService } from 'vs/platform/log/common/log'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { IListService, ListService } from 'vs/platform/list/browser/listService'; export interface IEditorContextViewService extends IContextViewService { dispose(): void; @@ -180,6 +181,8 @@ export class DynamicStandaloneServices extends Disposable { let contextKeyService = ensure(IContextKeyService, () => this._register(new ContextKeyService(configurationService))); + ensure(IListService, () => new ListService(contextKeyService)); + let commandService = ensure(ICommandService, () => new StandaloneCommandService(this._instantiationService)); ensure(IKeybindingService, () => this._register(new StandaloneKeybindingService(contextKeyService, commandService, telemetryService, notificationService, domElement))); diff --git a/src/vs/editor/standalone/common/themes.ts b/src/vs/editor/standalone/common/themes.ts index 0a51ad13ed8..5ac8ffea7e7 100644 --- a/src/vs/editor/standalone/common/themes.ts +++ b/src/vs/editor/standalone/common/themes.ts @@ -7,7 +7,7 @@ import { IStandaloneThemeData } from 'vs/editor/standalone/common/standaloneThemeService'; import { editorBackground, editorForeground, editorSelectionHighlight, editorInactiveSelection } from 'vs/platform/theme/common/colorRegistry'; -import { editorIndentGuides } from 'vs/editor/common/view/editorColorRegistry'; +import { editorIndentGuides, editorActiveIndentGuides } from 'vs/editor/common/view/editorColorRegistry'; /* -------------------------------- Begin vs theme -------------------------------- */ export const vs: IStandaloneThemeData = { @@ -74,6 +74,7 @@ export const vs: IStandaloneThemeData = { [editorForeground]: '#000000', [editorInactiveSelection]: '#E5EBF1', [editorIndentGuides]: '#D3D3D3', + [editorActiveIndentGuides]: '#939393', [editorSelectionHighlight]: '#ADD6FF4D' } }; @@ -144,6 +145,7 @@ export const vs_dark: IStandaloneThemeData = { [editorForeground]: '#D4D4D4', [editorInactiveSelection]: '#3A3D41', [editorIndentGuides]: '#404040', + [editorActiveIndentGuides]: '#707070', [editorSelectionHighlight]: '#ADD6FF26' } }; @@ -205,6 +207,7 @@ export const hc_black: IStandaloneThemeData = { [editorBackground]: '#000000', [editorForeground]: '#FFFFFF', [editorIndentGuides]: '#FFFFFF', + [editorActiveIndentGuides]: '#FFFFFF', } }; /* -------------------------------- End hc-black theme -------------------------------- */ diff --git a/src/vs/editor/test/browser/controller/cursor.test.ts b/src/vs/editor/test/browser/controller/cursor.test.ts index 9c62c90a5f3..c7e109750ea 100644 --- a/src/vs/editor/test/browser/controller/cursor.test.ts +++ b/src/vs/editor/test/browser/controller/cursor.test.ts @@ -11,18 +11,22 @@ import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; import { Handler, ICommand, IEditOperationBuilder, ICursorStateComputerData } from 'vs/editor/common/editorCommon'; -import { EndOfLinePreference, DefaultEndOfLine, ITextModelCreationOptions, ITextModel, EndOfLineSequence } from 'vs/editor/common/model'; +import { EndOfLinePreference, ITextModel, EndOfLineSequence } from 'vs/editor/common/model'; import { TextModel } from 'vs/editor/common/model/textModel'; import { IndentAction, IndentationRule } from 'vs/editor/common/modes/languageConfiguration'; import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; import { TestConfiguration } from 'vs/editor/test/common/mocks/testConfiguration'; import { MockMode } from 'vs/editor/test/common/mocks/mockMode'; -import { LanguageIdentifier } from 'vs/editor/common/modes'; +import { LanguageIdentifier, ITokenizationSupport, IState, TokenizationRegistry } from 'vs/editor/common/modes'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { CoreNavigationCommands, CoreEditingCommands } from 'vs/editor/browser/controller/coreCommands'; import { withTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; import { ViewModel } from 'vs/editor/common/viewModel/viewModelImpl'; -let H = Handler; +import { NULL_STATE } from 'vs/editor/common/modes/nullMode'; +import { TokenizationResult2 } from 'vs/editor/common/core/token'; +import { createTextModel, IRelaxedTextModelCreationOptions } from 'vs/editor/test/common/editorTestUtils'; + +const H = Handler; // --------- utils @@ -146,7 +150,7 @@ suite('Editor Controller - Cursor', () => { LINE4 + '\r\n' + LINE5; - thisModel = TextModel.createFromString(text); + thisModel = createTextModel(text); thisConfiguration = new TestConfiguration(null); thisViewModel = new ViewModel(0, thisConfiguration, thisModel, null); @@ -722,7 +726,7 @@ suite('Editor Controller - Cursor', () => { }); test('issue #4905 - column select is biased to the right', () => { - const model = TextModel.createFromString([ + const model = createTextModel([ 'var gulp = require("gulp");', 'var path = require("path");', 'var rimraf = require("rimraf");', @@ -758,7 +762,7 @@ suite('Editor Controller - Cursor', () => { }); test('issue #20087: column select with mouse', () => { - const model = TextModel.createFromString([ + const model = createTextModel([ '', '', '', @@ -820,7 +824,7 @@ suite('Editor Controller - Cursor', () => { }); test('issue #20087: column select with keyboard', () => { - const model = TextModel.createFromString([ + const model = createTextModel([ '', '', '', @@ -872,7 +876,7 @@ suite('Editor Controller - Cursor', () => { }); test('column select with keyboard', () => { - const model = TextModel.createFromString([ + const model = createTextModel([ 'var gulp = require("gulp");', 'var path = require("path");', 'var rimraf = require("rimraf");', @@ -1128,18 +1132,13 @@ class IndentRulesMode extends MockMode { suite('Editor Controller - Regression tests', () => { test('issue Microsoft/monaco-editor#443: Indentation of a single row deletes selected text in some cases', () => { - let model = TextModel.createFromString( + let model = createTextModel( [ 'Hello world!', 'another line' ].join('\n'), { - isForSimpleWidget: false, - defaultEOL: DefaultEndOfLine.LF, - detectIndentation: false, - insertSpaces: false, - tabSize: 4, - trimAutoWhitespace: false + insertSpaces: false }, ); @@ -1155,16 +1154,12 @@ suite('Editor Controller - Regression tests', () => { }); test('Bug 9121: Auto indent + undo + redo is funky', () => { - let model = TextModel.createFromString( + let model = createTextModel( [ '' ].join('\n'), { - isForSimpleWidget: false, - defaultEOL: DefaultEndOfLine.LF, - detectIndentation: false, insertSpaces: false, - tabSize: 4, trimAutoWhitespace: false }, ); @@ -1219,20 +1214,99 @@ suite('Editor Controller - Regression tests', () => { model.dispose(); }); + test('issue #47733: Undo mangles unicode characters', () => { + const languageId = new LanguageIdentifier('myMode', 3); + class MyMode extends MockMode { + constructor() { + super(languageId); + this._register(LanguageConfigurationRegistry.register(this.getLanguageIdentifier(), { + surroundingPairs: [{ open: '"', close: '"' }] + })); + } + } + + const mode = new MyMode(); + const model = createTextModel('\'👁\'', undefined, languageId); + + withTestCodeEditor(null, { model: model }, (editor, cursor) => { + editor.setSelection(new Selection(1, 1, 1, 2)); + + cursorCommand(cursor, H.Type, { text: '"' }, 'keyboard'); + assert.equal(model.getValue(EndOfLinePreference.LF), '"\'"👁\'', 'assert1'); + + cursorCommand(cursor, H.Undo, {}); + assert.equal(model.getValue(EndOfLinePreference.LF), '\'👁\'', 'assert2'); + }); + + model.dispose(); + mode.dispose(); + }); + + test('issue #46208: Allow empty selections in the undo/redo stack', () => { + let model = createTextModel(''); + + withTestCodeEditor(null, { model: model }, (editor, cursor) => { + cursorCommand(cursor, H.Type, { text: 'Hello' }, 'keyboard'); + cursorCommand(cursor, H.Type, { text: ' ' }, 'keyboard'); + cursorCommand(cursor, H.Type, { text: 'world' }, 'keyboard'); + cursorCommand(cursor, H.Type, { text: ' ' }, 'keyboard'); + assert.equal(model.getLineContent(1), 'Hello world '); + assertCursor(cursor, new Position(1, 13)); + + moveLeft(cursor); + moveRight(cursor); + + model.pushEditOperations([], [EditOperation.replaceMove(new Range(1, 12, 1, 13), '')], () => []); + assert.equal(model.getLineContent(1), 'Hello world'); + assertCursor(cursor, new Position(1, 12)); + + cursorCommand(cursor, H.Undo, {}); + assert.equal(model.getLineContent(1), 'Hello world '); + assertCursor(cursor, new Position(1, 13)); + + cursorCommand(cursor, H.Undo, {}); + assert.equal(model.getLineContent(1), 'Hello world'); + assertCursor(cursor, new Position(1, 12)); + + cursorCommand(cursor, H.Undo, {}); + assert.equal(model.getLineContent(1), 'Hello'); + assertCursor(cursor, new Position(1, 6)); + + cursorCommand(cursor, H.Undo, {}); + assert.equal(model.getLineContent(1), ''); + assertCursor(cursor, new Position(1, 1)); + + cursorCommand(cursor, H.Redo, {}); + assert.equal(model.getLineContent(1), 'Hello'); + assertCursor(cursor, new Position(1, 6)); + + cursorCommand(cursor, H.Redo, {}); + assert.equal(model.getLineContent(1), 'Hello world'); + assertCursor(cursor, new Position(1, 12)); + + cursorCommand(cursor, H.Redo, {}); + assert.equal(model.getLineContent(1), 'Hello world '); + assertCursor(cursor, new Position(1, 13)); + + cursorCommand(cursor, H.Redo, {}); + assert.equal(model.getLineContent(1), 'Hello world'); + assertCursor(cursor, new Position(1, 12)); + + cursorCommand(cursor, H.Redo, {}); + assert.equal(model.getLineContent(1), 'Hello world'); + assertCursor(cursor, new Position(1, 12)); + }); + + model.dispose(); + }); + test('bug #16815:Shift+Tab doesn\'t go back to tabstop', () => { let mode = new OnEnterMode(IndentAction.IndentOutdent); - let model = TextModel.createFromString( + let model = createTextModel( [ ' function baz() {' ].join('\n'), - { - isForSimpleWidget: false, - insertSpaces: true, - tabSize: 4, - detectIndentation: false, - defaultEOL: DefaultEndOfLine.LF, - trimAutoWhitespace: true - }, + undefined, mode.getLanguageIdentifier() ); @@ -1250,18 +1324,10 @@ suite('Editor Controller - Regression tests', () => { }); test('Bug #18293:[regression][editor] Can\'t outdent whitespace line', () => { - let model = TextModel.createFromString( + let model = createTextModel( [ ' ' - ].join('\n'), - { - isForSimpleWidget: false, - insertSpaces: true, - tabSize: 4, - detectIndentation: false, - defaultEOL: DefaultEndOfLine.LF, - trimAutoWhitespace: true - } + ].join('\n') ); withTestCodeEditor(null, { model: model }, (editor, cursor) => { @@ -1277,7 +1343,7 @@ suite('Editor Controller - Regression tests', () => { }); test('Bug #16657: [editor] Tab on empty line of zero indentation moves cursor to position (1,1)', () => { - let model = TextModel.createFromString( + let model = createTextModel( [ 'function baz() {', '\tfunction hello() { // something here', @@ -1288,12 +1354,7 @@ suite('Editor Controller - Regression tests', () => { '' ].join('\n'), { - isForSimpleWidget: false, - defaultEOL: DefaultEndOfLine.LF, - detectIndentation: false, insertSpaces: false, - tabSize: 4, - trimAutoWhitespace: true }, ); @@ -1353,8 +1414,7 @@ suite('Editor Controller - Regression tests', () => { text: [ 'hello' ], - languageIdentifier: mode.getLanguageIdentifier(), - modelOpts: { isForSimpleWidget: false, tabSize: 4, insertSpaces: true, detectIndentation: false, defaultEOL: DefaultEndOfLine.LF, trimAutoWhitespace: true } + languageIdentifier: mode.getLanguageIdentifier() }, (model, cursor) => { moveTo(cursor, 1, 3, false); moveTo(cursor, 1, 5, true); @@ -1371,20 +1431,12 @@ suite('Editor Controller - Regression tests', () => { test('issue #1140: Backspace stops prematurely', () => { let mode = new SurroundingMode(); - let model = TextModel.createFromString( + let model = createTextModel( [ 'function baz() {', ' return 1;', '};' - ].join('\n'), - { - isForSimpleWidget: false, - tabSize: 4, - insertSpaces: true, - detectIndentation: false, - defaultEOL: DefaultEndOfLine.LF, - trimAutoWhitespace: true - }, + ].join('\n') ); withTestCodeEditor(null, { model: model }, (editor, cursor) => { @@ -1492,21 +1544,61 @@ suite('Editor Controller - Regression tests', () => { }); }); + test('issue #46440: (1) Pasting a multi-line selection pastes entire selection into every insertion point', () => { + usingCursor({ + text: [ + 'line1', + 'line2', + 'line3' + ], + }, (model, cursor) => { + cursor.setSelections('test', [new Selection(1, 1, 1, 1), new Selection(2, 1, 2, 1), new Selection(3, 1, 3, 1)]); + + cursorCommand(cursor, H.Paste, { + text: 'a\nb\nc', + pasteOnNewLine: false, + multicursorText: null + }); + + assert.equal(model.getValue(), [ + 'aline1', + 'bline2', + 'cline3' + ].join('\n')); + }); + }); + + test('issue #46440: (2) Pasting a multi-line selection pastes entire selection into every insertion point', () => { + usingCursor({ + text: [ + 'line1', + 'line2', + 'line3' + ], + }, (model, cursor) => { + cursor.setSelections('test', [new Selection(1, 1, 1, 1), new Selection(2, 1, 2, 1), new Selection(3, 1, 3, 1)]); + + cursorCommand(cursor, H.Paste, { + text: 'a\nb\nc\n', + pasteOnNewLine: false, + multicursorText: null + }); + + assert.equal(model.getValue(), [ + 'aline1', + 'bline2', + 'cline3' + ].join('\n')); + }); + }); + test('issue #3071: Investigate why undo stack gets corrupted', () => { - let model = TextModel.createFromString( + let model = createTextModel( [ 'some lines', 'and more lines', 'just some text', - ].join('\n'), - { - isForSimpleWidget: false, - insertSpaces: true, - tabSize: 4, - detectIndentation: false, - defaultEOL: DefaultEndOfLine.LF, - trimAutoWhitespace: true - } + ].join('\n') ); withTestCodeEditor(null, { model: model }, (editor, cursor) => { @@ -1558,8 +1650,7 @@ suite('Editor Controller - Regression tests', () => { 'and more lines', 'just some text', ], - languageIdentifier: null, - modelOpts: { isForSimpleWidget: false, insertSpaces: true, tabSize: 4, detectIndentation: false, defaultEOL: DefaultEndOfLine.LF, trimAutoWhitespace: true } + languageIdentifier: null }, (model, cursor) => { moveTo(cursor, 3, 1, false); @@ -1574,22 +1665,14 @@ suite('Editor Controller - Regression tests', () => { }); test('issue #3463: pressing tab adds spaces, but not as many as for a tab', () => { - let model = TextModel.createFromString( + let model = createTextModel( [ 'function a() {', '\tvar a = {', '\t\tx: 3', '\t};', '}', - ].join('\n'), - { - isForSimpleWidget: false, - insertSpaces: true, - tabSize: 4, - detectIndentation: false, - defaultEOL: DefaultEndOfLine.LF, - trimAutoWhitespace: true - } + ].join('\n') ); withTestCodeEditor(null, { model: model }, (editor, cursor) => { @@ -1602,18 +1685,13 @@ suite('Editor Controller - Regression tests', () => { }); test('issue #4312: trying to type a tab character over a sequence of spaces results in unexpected behaviour', () => { - let model = TextModel.createFromString( + let model = createTextModel( [ 'var foo = 123; // this is a comment', 'var bar = 4; // another comment' ].join('\n'), { - isForSimpleWidget: false, insertSpaces: false, - tabSize: 4, - detectIndentation: false, - defaultEOL: DefaultEndOfLine.LF, - trimAutoWhitespace: true } ); @@ -1707,7 +1785,7 @@ suite('Editor Controller - Regression tests', () => { }); test('issue #33788: Wrong cursor position when double click to select a word', () => { - let model = TextModel.createFromString( + let model = createTextModel( [ 'Just some text' ].join('\n') @@ -1724,6 +1802,21 @@ suite('Editor Controller - Regression tests', () => { model.dispose(); }); + test('issue #12887: Double-click highlighting separating white space', () => { + let model = createTextModel( + [ + 'abc def' + ].join('\n') + ); + + withTestCodeEditor(null, { model: model }, (editor, cursor) => { + CoreNavigationCommands.WordSelect.runCoreEditorCommand(cursor, { position: new Position(1, 5) }); + assert.deepEqual(cursor.getSelection(), new Selection(1, 5, 1, 8)); + }); + + model.dispose(); + }); + test('issue #9675: Undo/Redo adds a stop in between CHN Characters', () => { usingCursor({ text: [ @@ -1863,7 +1956,7 @@ suite('Editor Controller - Regression tests', () => { }); test('issue #41573 - delete across multiple lines does not shrink the selection when word wraps', () => { - const model = TextModel.createFromString([ + const model = createTextModel([ 'Authorization: \'Bearer pHKRfCTFSnGxs6akKlb9ddIXcca0sIUSZJutPHYqz7vEeHdMTMh0SGN0IGU3a0n59DXjTLRsj5EJ2u33qLNIFi9fk5XF8pK39PndLYUZhPt4QvHGLScgSkK0L4gwzkzMloTQPpKhqiikiIOvyNNSpd2o8j29NnOmdTUOKi9DVt74PD2ohKxyOrWZ6oZprTkb3eKajcpnS0LABKfaw2rmv4\',' ].join('\n')); const config = new TestConfiguration({ @@ -1915,18 +2008,10 @@ suite('Editor Controller - Regression tests', () => { }); test('issue #44805: Should not be able to undo in readonly editor', () => { - let model = TextModel.createFromString( + let model = createTextModel( [ '' - ].join('\n'), - { - isForSimpleWidget: false, - defaultEOL: DefaultEndOfLine.LF, - detectIndentation: false, - insertSpaces: false, - tabSize: 4, - trimAutoWhitespace: true - } + ].join('\n') ); withTestCodeEditor(null, { readOnly: true, model: model }, (editor, cursor) => { @@ -1942,6 +2027,141 @@ suite('Editor Controller - Regression tests', () => { model.dispose(); }); + + test('issue #46314: ViewModel is out of sync with Model!', () => { + + const tokenizationSupport: ITokenizationSupport = { + getInitialState: () => NULL_STATE, + tokenize: undefined, + tokenize2: (line: string, state: IState): TokenizationResult2 => { + return new TokenizationResult2(null, state); + } + }; + + const LANGUAGE_ID = 'modelModeTest1'; + const languageRegistration = TokenizationRegistry.register(LANGUAGE_ID, tokenizationSupport); + let model = createTextModel('Just text', undefined, new LanguageIdentifier(LANGUAGE_ID, 0)); + + withTestCodeEditor(null, { model: model }, (editor1, cursor1) => { + withTestCodeEditor(null, { model: model }, (editor2, cursor2) => { + + editor1.onDidChangeCursorPosition(() => { + model.tokenizeIfCheap(1); + }); + + model.applyEdits([{ range: new Range(1, 1, 1, 1), text: '-' }]); + }); + }); + + languageRegistration.dispose(); + model.dispose(); + }); + + test('issue #37967: problem replacing consecutive characters', () => { + let model = createTextModel( + [ + 'const a = "foo";', + 'const b = ""' + ].join('\n') + ); + + withTestCodeEditor(null, { multiCursorMergeOverlapping: false, model: model }, (editor, cursor) => { + editor.setSelections([ + new Selection(1, 12, 1, 12), + new Selection(1, 16, 1, 16), + new Selection(2, 12, 2, 12), + new Selection(2, 13, 2, 13), + ]); + + CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); + + assertCursor(cursor, [ + new Selection(1, 11, 1, 11), + new Selection(1, 14, 1, 14), + new Selection(2, 11, 2, 11), + new Selection(2, 11, 2, 11), + ]); + + cursorCommand(cursor, H.Type, { text: '\'' }, 'keyboard'); + + assert.equal(model.getLineContent(1), 'const a = \'foo\';'); + assert.equal(model.getLineContent(2), 'const b = \'\''); + }); + + model.dispose(); + }); + + test('issue #15761: Cursor doesn\'t move in a redo operation', () => { + let model = createTextModel( + [ + 'hello' + ].join('\n') + ); + + withTestCodeEditor(null, { model: model }, (editor, cursor) => { + editor.setSelections([ + new Selection(1, 4, 1, 4) + ]); + + editor.executeEdits('test', [{ + range: new Range(1, 1, 1, 1), + text: '*', + forceMoveMarkers: true + }]); + assertCursor(cursor, [ + new Selection(1, 5, 1, 5), + ]); + + cursorCommand(cursor, H.Undo, null, 'keyboard'); + assertCursor(cursor, [ + new Selection(1, 4, 1, 4), + ]); + + cursorCommand(cursor, H.Redo, null, 'keyboard'); + assertCursor(cursor, [ + new Selection(1, 5, 1, 5), + ]); + }); + + model.dispose(); + }); + + test('issue #42783: API Calls with Undo Leave Cursor in Wrong Position', () => { + let model = createTextModel( + [ + 'ab' + ].join('\n') + ); + + withTestCodeEditor(null, { model: model }, (editor, cursor) => { + editor.setSelections([ + new Selection(1, 1, 1, 1) + ]); + + editor.executeEdits('test', [{ + range: new Range(1, 1, 1, 3), + text: '' + }]); + assertCursor(cursor, [ + new Selection(1, 1, 1, 1), + ]); + + cursorCommand(cursor, H.Undo, null, 'keyboard'); + assertCursor(cursor, [ + new Selection(1, 1, 1, 1), + ]); + + editor.executeEdits('test', [{ + range: new Range(1, 1, 1, 2), + text: '' + }]); + assertCursor(cursor, [ + new Selection(1, 1, 1, 1), + ]); + }); + + model.dispose(); + }); }); suite('Editor Controller - Cursor Configuration', () => { @@ -1954,8 +2174,7 @@ suite('Editor Controller - Cursor Configuration', () => { ' Third Line', '', '1' - ], - modelOpts: { isForSimpleWidget: false, insertSpaces: true, tabSize: 4, detectIndentation: false, defaultEOL: DefaultEndOfLine.LF, trimAutoWhitespace: true } + ] }, (model, cursor) => { CoreNavigationCommands.MoveTo.runCoreEditorCommand(cursor, { position: new Position(1, 21), source: 'keyboard' }); cursorCommand(cursor, H.Type, { text: '\n' }, 'keyboard'); @@ -1965,7 +2184,7 @@ suite('Editor Controller - Cursor Configuration', () => { }); test('Cursor honors insertSpaces configuration on tab', () => { - let model = TextModel.createFromString( + let model = createTextModel( [ ' \tMy First Line\t ', 'My Second Line123', @@ -1974,12 +2193,7 @@ suite('Editor Controller - Cursor Configuration', () => { '1' ].join('\n'), { - isForSimpleWidget: false, - insertSpaces: true, tabSize: 13, - detectIndentation: false, - defaultEOL: DefaultEndOfLine.LF, - trimAutoWhitespace: true } ); @@ -2048,8 +2262,7 @@ suite('Editor Controller - Cursor Configuration', () => { text: [ '\thello' ], - languageIdentifier: mode.getLanguageIdentifier(), - modelOpts: { isForSimpleWidget: false, insertSpaces: true, tabSize: 4, detectIndentation: false, defaultEOL: DefaultEndOfLine.LF, trimAutoWhitespace: true } + languageIdentifier: mode.getLanguageIdentifier() }, (model, cursor) => { moveTo(cursor, 1, 7, false); assertCursor(cursor, new Selection(1, 7, 1, 7)); @@ -2066,8 +2279,7 @@ suite('Editor Controller - Cursor Configuration', () => { text: [ '\thello' ], - languageIdentifier: mode.getLanguageIdentifier(), - modelOpts: { isForSimpleWidget: false, insertSpaces: true, tabSize: 4, detectIndentation: false, defaultEOL: DefaultEndOfLine.LF, trimAutoWhitespace: true } + languageIdentifier: mode.getLanguageIdentifier() }, (model, cursor) => { moveTo(cursor, 1, 7, false); assertCursor(cursor, new Selection(1, 7, 1, 7)); @@ -2084,8 +2296,7 @@ suite('Editor Controller - Cursor Configuration', () => { text: [ '\thell()' ], - languageIdentifier: mode.getLanguageIdentifier(), - modelOpts: { isForSimpleWidget: false, insertSpaces: true, tabSize: 4, detectIndentation: false, defaultEOL: DefaultEndOfLine.LF, trimAutoWhitespace: true } + languageIdentifier: mode.getLanguageIdentifier() }, (model, cursor) => { moveTo(cursor, 1, 7, false); assertCursor(cursor, new Selection(1, 7, 1, 7)); @@ -2102,11 +2313,6 @@ suite('Editor Controller - Cursor Configuration', () => { ' some line abc ' ], modelOpts: { - isForSimpleWidget: false, - insertSpaces: true, - tabSize: 4, - detectIndentation: false, - defaultEOL: DefaultEndOfLine.LF, trimAutoWhitespace: false } }, (model, cursor) => { @@ -2129,15 +2335,7 @@ suite('Editor Controller - Cursor Configuration', () => { usingCursor({ text: [ ' ' - ], - modelOpts: { - isForSimpleWidget: false, - insertSpaces: true, - tabSize: 4, - detectIndentation: false, - defaultEOL: DefaultEndOfLine.LF, - trimAutoWhitespace: true - } + ] }, (model, cursor) => { moveTo(cursor, 1, model.getLineContent(1).length + 1); cursorCommand(cursor, H.Type, { text: '\n' }, 'keyboard'); @@ -2157,14 +2355,6 @@ suite('Editor Controller - Cursor Configuration', () => { text: [ 'function foo (params: string) {}' ], - modelOpts: { - isForSimpleWidget: false, - insertSpaces: true, - tabSize: 4, - detectIndentation: false, - defaultEOL: DefaultEndOfLine.LF, - trimAutoWhitespace: true - }, languageIdentifier: mode.getLanguageIdentifier(), }, (model, cursor) => { @@ -2198,22 +2388,14 @@ suite('Editor Controller - Cursor Configuration', () => { }); test('removeAutoWhitespace on: removes only whitespace the cursor added 2', () => { - let model = TextModel.createFromString( + let model = createTextModel( [ ' if (a) {', ' ', '', '', ' }' - ].join('\n'), - { - isForSimpleWidget: false, - insertSpaces: true, - tabSize: 4, - detectIndentation: false, - defaultEOL: DefaultEndOfLine.LF, - trimAutoWhitespace: true - } + ].join('\n') ); withTestCodeEditor(null, { model: model }, (editor, cursor) => { @@ -2247,18 +2429,10 @@ suite('Editor Controller - Cursor Configuration', () => { }); test('removeAutoWhitespace on: test 1', () => { - let model = TextModel.createFromString( + let model = createTextModel( [ ' some line abc ' - ].join('\n'), - { - isForSimpleWidget: false, - insertSpaces: true, - tabSize: 4, - detectIndentation: false, - defaultEOL: DefaultEndOfLine.LF, - trimAutoWhitespace: true - } + ].join('\n') ); withTestCodeEditor(null, { model: model }, (editor, cursor) => { @@ -2312,20 +2486,12 @@ suite('Editor Controller - Cursor Configuration', () => { }); test('UseTabStops is off', () => { - let model = TextModel.createFromString( + let model = createTextModel( [ ' x', ' a ', ' ' - ].join('\n'), - { - isForSimpleWidget: false, - insertSpaces: true, - tabSize: 4, - detectIndentation: false, - defaultEOL: DefaultEndOfLine.LF, - trimAutoWhitespace: true - } + ].join('\n') ); withTestCodeEditor(null, { model: model, useTabStops: false }, (editor, cursor) => { @@ -2339,20 +2505,12 @@ suite('Editor Controller - Cursor Configuration', () => { }); test('Backspace removes whitespaces with tab size', () => { - let model = TextModel.createFromString( + let model = createTextModel( [ ' \t \t x', ' a ', ' ' - ].join('\n'), - { - isForSimpleWidget: false, - insertSpaces: true, - tabSize: 4, - detectIndentation: false, - defaultEOL: DefaultEndOfLine.LF, - trimAutoWhitespace: true - } + ].join('\n') ); withTestCodeEditor(null, { model: model, useTabStops: true }, (editor, cursor) => { @@ -2413,17 +2571,12 @@ suite('Editor Controller - Cursor Configuration', () => { }); test('PR #5423: Auto indent + undo + redo is funky', () => { - let model = TextModel.createFromString( + let model = createTextModel( [ '' ].join('\n'), { - isForSimpleWidget: false, - defaultEOL: DefaultEndOfLine.LF, - detectIndentation: false, insertSpaces: false, - tabSize: 4, - trimAutoWhitespace: true } ); @@ -2499,7 +2652,7 @@ suite('Editor Controller - Indentation Rules', () => { '\tif (true) {' ], languageIdentifier: mode.getLanguageIdentifier(), - modelOpts: { isForSimpleWidget: false, insertSpaces: false, tabSize: 4, detectIndentation: false, defaultEOL: DefaultEndOfLine.LF, trimAutoWhitespace: true }, + modelOpts: { insertSpaces: false }, editorOpts: { autoIndent: true } }, (model, cursor) => { moveTo(cursor, 1, 12, false); @@ -2523,7 +2676,6 @@ suite('Editor Controller - Indentation Rules', () => { '\t' ], languageIdentifier: mode.getLanguageIdentifier(), - modelOpts: { isForSimpleWidget: false, insertSpaces: false, tabSize: 4, detectIndentation: false, defaultEOL: DefaultEndOfLine.LF, trimAutoWhitespace: true }, editorOpts: { autoIndent: true } }, (model, cursor) => { moveTo(cursor, 2, 2, false); @@ -2542,7 +2694,7 @@ suite('Editor Controller - Indentation Rules', () => { '\t\t\treturn true' ], languageIdentifier: mode.getLanguageIdentifier(), - modelOpts: { isForSimpleWidget: false, insertSpaces: false, tabSize: 4, detectIndentation: false, defaultEOL: DefaultEndOfLine.LF, trimAutoWhitespace: true }, + modelOpts: { insertSpaces: false }, editorOpts: { autoIndent: true } }, (model, cursor) => { moveTo(cursor, 2, 15, false); @@ -2562,7 +2714,7 @@ suite('Editor Controller - Indentation Rules', () => { '\t\t\t\treturn true' ], languageIdentifier: mode.getLanguageIdentifier(), - modelOpts: { isForSimpleWidget: false, insertSpaces: false, tabSize: 4, detectIndentation: false, defaultEOL: DefaultEndOfLine.LF, trimAutoWhitespace: true }, + modelOpts: { insertSpaces: false }, editorOpts: { autoIndent: true } }, (model, cursor) => { moveTo(cursor, 2, 14, false); @@ -2580,18 +2732,13 @@ suite('Editor Controller - Indentation Rules', () => { }); test('Enter honors indentNextLinePattern 2', () => { - let model = TextModel.createFromString( + let model = createTextModel( [ 'if (true)', '\tif (true)' ].join('\n'), { - isForSimpleWidget: false, - defaultEOL: DefaultEndOfLine.LF, - detectIndentation: false, insertSpaces: false, - tabSize: 4, - trimAutoWhitespace: true }, mode.getLanguageIdentifier() ); @@ -2620,7 +2767,6 @@ suite('Editor Controller - Indentation Rules', () => { '}}' ], languageIdentifier: mode.getLanguageIdentifier(), - modelOpts: { isForSimpleWidget: false, insertSpaces: false, tabSize: 4, detectIndentation: false, defaultEOL: DefaultEndOfLine.LF, trimAutoWhitespace: true }, editorOpts: { autoIndent: true } }, (model, cursor) => { moveTo(cursor, 3, 13, false); @@ -2641,7 +2787,7 @@ suite('Editor Controller - Indentation Rules', () => { '\t}a}' ], languageIdentifier: mode.getLanguageIdentifier(), - modelOpts: { isForSimpleWidget: false, insertSpaces: false, tabSize: 4, detectIndentation: false, defaultEOL: DefaultEndOfLine.LF, trimAutoWhitespace: true } + modelOpts: { insertSpaces: false } }, (model, cursor) => { moveTo(cursor, 4, 3, false); moveTo(cursor, 4, 4, true); @@ -2660,7 +2806,7 @@ suite('Editor Controller - Indentation Rules', () => { '\tif (true) {' ], languageIdentifier: mode.getLanguageIdentifier(), - modelOpts: { isForSimpleWidget: false, insertSpaces: false, tabSize: 4, detectIndentation: false, defaultEOL: DefaultEndOfLine.LF, trimAutoWhitespace: true } + modelOpts: { insertSpaces: false } }, (model, cursor) => { moveTo(cursor, 2, 12, false); moveTo(cursor, 2, 13, true); @@ -2681,7 +2827,6 @@ suite('Editor Controller - Indentation Rules', () => { '\tif (true) {' ], languageIdentifier: mode.getLanguageIdentifier(), - modelOpts: { isForSimpleWidget: false, insertSpaces: true, tabSize: 4, detectIndentation: false, defaultEOL: DefaultEndOfLine.LF, trimAutoWhitespace: true } }, (model, cursor) => { moveTo(cursor, 1, 12, false); assertCursor(cursor, new Selection(1, 12, 1, 12)); @@ -2706,7 +2851,6 @@ suite('Editor Controller - Indentation Rules', () => { ' if (true) {' ], languageIdentifier: mode.getLanguageIdentifier(), - modelOpts: { isForSimpleWidget: false, insertSpaces: true, tabSize: 4, detectIndentation: false, defaultEOL: DefaultEndOfLine.LF, trimAutoWhitespace: true } }, (model, cursor) => { moveTo(cursor, 1, 12, false); assertCursor(cursor, new Selection(1, 12, 1, 12)); @@ -2730,7 +2874,7 @@ suite('Editor Controller - Indentation Rules', () => { ' if (true) {' ], languageIdentifier: mode.getLanguageIdentifier(), - modelOpts: { isForSimpleWidget: false, insertSpaces: false, tabSize: 4, detectIndentation: false, defaultEOL: DefaultEndOfLine.LF, trimAutoWhitespace: true } + modelOpts: { insertSpaces: false } }, (model, cursor) => { moveTo(cursor, 1, 12, false); assertCursor(cursor, new Selection(1, 12, 1, 12)); @@ -2758,7 +2902,7 @@ suite('Editor Controller - Indentation Rules', () => { '\t}' ], languageIdentifier: mode.getLanguageIdentifier(), - modelOpts: { isForSimpleWidget: false, insertSpaces: false, tabSize: 4, detectIndentation: false, defaultEOL: DefaultEndOfLine.LF, trimAutoWhitespace: true }, + modelOpts: { insertSpaces: false }, editorOpts: { autoIndent: true } }, (model, cursor) => { moveTo(cursor, 5, 4, false); @@ -2779,7 +2923,7 @@ suite('Editor Controller - Indentation Rules', () => { '\t}a}' ], languageIdentifier: mode.getLanguageIdentifier(), - modelOpts: { isForSimpleWidget: false, insertSpaces: false, tabSize: 4, detectIndentation: false, defaultEOL: DefaultEndOfLine.LF, trimAutoWhitespace: true } + modelOpts: { insertSpaces: false } }, (model, cursor) => { moveTo(cursor, 3, 9, false); assertCursor(cursor, new Selection(3, 9, 3, 9)); @@ -2799,7 +2943,7 @@ suite('Editor Controller - Indentation Rules', () => { '\t}a}' ], languageIdentifier: mode.getLanguageIdentifier(), - modelOpts: { isForSimpleWidget: false, insertSpaces: false, tabSize: 4, detectIndentation: false, defaultEOL: DefaultEndOfLine.LF, trimAutoWhitespace: true } + modelOpts: { insertSpaces: false } }, (model, cursor) => { moveTo(cursor, 3, 3, false); assertCursor(cursor, new Selection(3, 3, 3, 3)); @@ -2818,8 +2962,7 @@ suite('Editor Controller - Indentation Rules', () => { ' return true;', ' }a}' ], - languageIdentifier: mode.getLanguageIdentifier(), - modelOpts: { isForSimpleWidget: false, insertSpaces: true, tabSize: 2, detectIndentation: false, defaultEOL: DefaultEndOfLine.LF, trimAutoWhitespace: true } + languageIdentifier: mode.getLanguageIdentifier() }, (model, cursor) => { moveTo(cursor, 3, 11, false); assertCursor(cursor, new Selection(3, 11, 3, 11)); @@ -2839,7 +2982,7 @@ suite('Editor Controller - Indentation Rules', () => { '\t}a}' ], languageIdentifier: mode.getLanguageIdentifier(), - modelOpts: { isForSimpleWidget: false, insertSpaces: false, tabSize: 4, detectIndentation: false, defaultEOL: DefaultEndOfLine.LF, trimAutoWhitespace: true } + modelOpts: { insertSpaces: false } }, (model, cursor) => { moveTo(cursor, 3, 2, false); assertCursor(cursor, new Selection(3, 2, 3, 2)); @@ -2866,7 +3009,7 @@ suite('Editor Controller - Indentation Rules', () => { '\t\t}a}' ], languageIdentifier: mode.getLanguageIdentifier(), - modelOpts: { isForSimpleWidget: false, insertSpaces: false, tabSize: 4, detectIndentation: false, defaultEOL: DefaultEndOfLine.LF, trimAutoWhitespace: true } + modelOpts: { insertSpaces: false } }, (model, cursor) => { moveTo(cursor, 3, 4, false); assertCursor(cursor, new Selection(3, 4, 3, 4)); @@ -2892,8 +3035,7 @@ suite('Editor Controller - Indentation Rules', () => { ' return true;', '}a}' ], - languageIdentifier: mode.getLanguageIdentifier(), - modelOpts: { isForSimpleWidget: false, insertSpaces: true, tabSize: 2, detectIndentation: false, defaultEOL: DefaultEndOfLine.LF, trimAutoWhitespace: true } + languageIdentifier: mode.getLanguageIdentifier() }, (model, cursor) => { moveTo(cursor, 3, 2, false); assertCursor(cursor, new Selection(3, 2, 3, 2)); @@ -2923,7 +3065,7 @@ suite('Editor Controller - Indentation Rules', () => { '}a}' ], languageIdentifier: mode.getLanguageIdentifier(), - modelOpts: { isForSimpleWidget: false, insertSpaces: true, tabSize: 2, detectIndentation: false, defaultEOL: DefaultEndOfLine.LF, trimAutoWhitespace: true } + modelOpts: { tabSize: 2 } }, (model, cursor) => { moveTo(cursor, 3, 3, false); assertCursor(cursor, new Selection(3, 3, 3, 3)); @@ -2949,7 +3091,7 @@ suite('Editor Controller - Indentation Rules', () => { '' ], languageIdentifier: mode.getLanguageIdentifier(), - modelOpts: { isForSimpleWidget: false, insertSpaces: true, tabSize: 2, detectIndentation: false, defaultEOL: DefaultEndOfLine.LF, trimAutoWhitespace: true } + modelOpts: { tabSize: 2 } }, (model, cursor) => { moveTo(cursor, 3, 5, false); moveTo(cursor, 4, 3, true); @@ -2970,12 +3112,7 @@ suite('Editor Controller - Indentation Rules', () => { '}' ], modelOpts: { - isForSimpleWidget: false, - defaultEOL: DefaultEndOfLine.LF, - detectIndentation: false, insertSpaces: false, - tabSize: 4, - trimAutoWhitespace: true }, languageIdentifier: mode.getLanguageIdentifier(), }, (model, cursor) => { @@ -2998,12 +3135,7 @@ suite('Editor Controller - Indentation Rules', () => { '}' ], modelOpts: { - isForSimpleWidget: false, - defaultEOL: DefaultEndOfLine.LF, - detectIndentation: false, insertSpaces: false, - tabSize: 4, - trimAutoWhitespace: true }, languageIdentifier: mode.getLanguageIdentifier(), }, (model, cursor) => { @@ -3027,7 +3159,7 @@ suite('Editor Controller - Indentation Rules', () => { '\t}', '?>' ], - modelOpts: { isForSimpleWidget: false, insertSpaces: false, tabSize: 4, detectIndentation: false, defaultEOL: DefaultEndOfLine.LF, trimAutoWhitespace: true } + modelOpts: { insertSpaces: false } }, (model, cursor) => { moveTo(cursor, 5, 3, false); assertCursor(cursor, new Selection(5, 3, 5, 3)); @@ -3046,7 +3178,7 @@ suite('Editor Controller - Indentation Rules', () => { ' return 5;', ' ' ], - modelOpts: { isForSimpleWidget: false, insertSpaces: false, tabSize: 4, detectIndentation: false, defaultEOL: DefaultEndOfLine.LF, trimAutoWhitespace: true } + modelOpts: { insertSpaces: false } }, (model, cursor) => { moveTo(cursor, 3, 2, false); assertCursor(cursor, new Selection(3, 2, 3, 2)); @@ -3058,7 +3190,7 @@ suite('Editor Controller - Indentation Rules', () => { }); test('bug #16543: Tab should indent to correct indentation spot immediately', () => { - let model = TextModel.createFromString( + let model = createTextModel( [ 'function baz() {', '\tfunction hello() { // something here', @@ -3068,12 +3200,7 @@ suite('Editor Controller - Indentation Rules', () => { '}' ].join('\n'), { - isForSimpleWidget: false, - defaultEOL: DefaultEndOfLine.LF, - detectIndentation: false, insertSpaces: false, - tabSize: 4, - trimAutoWhitespace: true }, mode.getLanguageIdentifier() ); @@ -3091,7 +3218,7 @@ suite('Editor Controller - Indentation Rules', () => { test('bug #2938 (1): When pressing Tab on white-space only lines, indent straight to the right spot (similar to empty lines)', () => { - let model = TextModel.createFromString( + let model = createTextModel( [ '\tfunction baz() {', '\t\tfunction hello() { // something here', @@ -3101,12 +3228,7 @@ suite('Editor Controller - Indentation Rules', () => { '\t}' ].join('\n'), { - isForSimpleWidget: false, - defaultEOL: DefaultEndOfLine.LF, - detectIndentation: false, insertSpaces: false, - tabSize: 4, - trimAutoWhitespace: true }, mode.getLanguageIdentifier() ); @@ -3124,7 +3246,7 @@ suite('Editor Controller - Indentation Rules', () => { test('bug #2938 (2): When pressing Tab on white-space only lines, indent straight to the right spot (similar to empty lines)', () => { - let model = TextModel.createFromString( + let model = createTextModel( [ '\tfunction baz() {', '\t\tfunction hello() { // something here', @@ -3134,12 +3256,7 @@ suite('Editor Controller - Indentation Rules', () => { '\t}' ].join('\n'), { - isForSimpleWidget: false, - defaultEOL: DefaultEndOfLine.LF, - detectIndentation: false, insertSpaces: false, - tabSize: 4, - trimAutoWhitespace: true }, mode.getLanguageIdentifier() ); @@ -3156,7 +3273,7 @@ suite('Editor Controller - Indentation Rules', () => { }); test('bug #2938 (3): When pressing Tab on white-space only lines, indent straight to the right spot (similar to empty lines)', () => { - let model = TextModel.createFromString( + let model = createTextModel( [ '\tfunction baz() {', '\t\tfunction hello() { // something here', @@ -3166,12 +3283,7 @@ suite('Editor Controller - Indentation Rules', () => { '\t}' ].join('\n'), { - isForSimpleWidget: false, - defaultEOL: DefaultEndOfLine.LF, - detectIndentation: false, insertSpaces: false, - tabSize: 4, - trimAutoWhitespace: true }, mode.getLanguageIdentifier() ); @@ -3188,7 +3300,7 @@ suite('Editor Controller - Indentation Rules', () => { }); test('bug #2938 (4): When pressing Tab on white-space only lines, indent straight to the right spot (similar to empty lines)', () => { - let model = TextModel.createFromString( + let model = createTextModel( [ '\tfunction baz() {', '\t\tfunction hello() { // something here', @@ -3198,12 +3310,7 @@ suite('Editor Controller - Indentation Rules', () => { '\t}' ].join('\n'), { - isForSimpleWidget: false, - defaultEOL: DefaultEndOfLine.LF, - detectIndentation: false, insertSpaces: false, - tabSize: 4, - trimAutoWhitespace: true }, mode.getLanguageIdentifier() ); @@ -3221,7 +3328,7 @@ suite('Editor Controller - Indentation Rules', () => { test('bug #31015: When pressing Tab on lines and Enter rules are avail, indent straight to the right spotTab', () => { let mode = new OnEnterMode(IndentAction.Indent); - let model = TextModel.createFromString( + let model = createTextModel( [ ' if (a) {', ' ', @@ -3229,14 +3336,7 @@ suite('Editor Controller - Indentation Rules', () => { '', ' }' ].join('\n'), - { - isForSimpleWidget: false, - insertSpaces: true, - tabSize: 4, - detectIndentation: false, - defaultEOL: DefaultEndOfLine.LF, - trimAutoWhitespace: true - }, + undefined, mode.getLanguageIdentifier() ); @@ -3259,21 +3359,14 @@ suite('Editor Controller - Indentation Rules', () => { increaseIndentPattern: /^\s*((begin|class|def|else|elsif|ensure|for|if|module|rescue|unless|until|when|while)|(.*\sdo\b))\b[^\{;]*$/, decreaseIndentPattern: /^\s*([}\]]([,)]?\s*(#|$)|\.[a-zA-Z_]\w*\b)|(end|rescue|ensure|else|elsif|when)\b)/ }); - let model = TextModel.createFromString( + let model = createTextModel( [ 'class Greeter', ' def initialize(name)', ' @name = name', ' en' ].join('\n'), - { - isForSimpleWidget: false, - defaultEOL: DefaultEndOfLine.LF, - detectIndentation: false, - insertSpaces: true, - tabSize: 2, - trimAutoWhitespace: true - }, + undefined, rubyMode.getLanguageIdentifier() ); @@ -3298,8 +3391,7 @@ suite('Editor Controller - Indentation Rules', () => { '\t\tconsole.log()', '\t}' ], - languageIdentifier: mode.getLanguageIdentifier(), - modelOpts: { isForSimpleWidget: false, insertSpaces: false, tabSize: 4, detectIndentation: false, defaultEOL: DefaultEndOfLine.LF, trimAutoWhitespace: true } + languageIdentifier: mode.getLanguageIdentifier() }, (model, cursor) => { moveTo(cursor, 5, 3, false); assertCursor(cursor, new Selection(5, 3, 5, 3)); @@ -3320,8 +3412,7 @@ suite('Editor Controller - Indentation Rules', () => { ') {', '}' ], - languageIdentifier: mode.getLanguageIdentifier(), - modelOpts: { isForSimpleWidget: false, insertSpaces: false, tabSize: 4, detectIndentation: false, defaultEOL: DefaultEndOfLine.LF, trimAutoWhitespace: true } + languageIdentifier: mode.getLanguageIdentifier() }, (model, cursor) => { moveTo(cursor, 2, 3, false); assertCursor(cursor, new Selection(2, 3, 2, 3)); @@ -3340,7 +3431,6 @@ suite('Editor Controller - Indentation Rules', () => { '\t\t' ], languageIdentifier: mode.getLanguageIdentifier(), - modelOpts: { isForSimpleWidget: false, insertSpaces: false, tabSize: 4, detectIndentation: false, defaultEOL: DefaultEndOfLine.LF, trimAutoWhitespace: true }, editorOpts: { autoIndent: true } }, (model, cursor) => { moveTo(cursor, 3, 3, false); @@ -3399,7 +3489,7 @@ suite('Editor Controller - Indentation Rules', () => { } let mode = new JSMode(); - let model = TextModel.createFromString( + let model = createTextModel( [ 'class ItemCtrl {', ' getPropertiesByItemId(id) {', @@ -3459,7 +3549,7 @@ suite('Editor Controller - Indentation Rules', () => { } let mode = new CppMode(); - let model = TextModel.createFromString( + let model = createTextModel( [ 'int main() {', ' return 0;', @@ -3471,7 +3561,7 @@ suite('Editor Controller - Indentation Rules', () => { '', ')', ].join('\n'), - { isForSimpleWidget: false, insertSpaces: true, detectIndentation: false, tabSize: 2, trimAutoWhitespace: false, defaultEOL: DefaultEndOfLine.LF }, + { tabSize: 2 }, mode.getLanguageIdentifier() ); @@ -3504,12 +3594,12 @@ suite('Editor Controller - Indentation Rules', () => { interface ICursorOpts { text: string[]; languageIdentifier?: LanguageIdentifier; - modelOpts?: ITextModelCreationOptions; + modelOpts?: IRelaxedTextModelCreationOptions; editorOpts?: IEditorOptions; } function usingCursor(opts: ICursorOpts, callback: (model: TextModel, cursor: Cursor) => void): void { - let model = TextModel.createFromString(opts.text.join('\n'), opts.modelOpts, opts.languageIdentifier); + let model = createTextModel(opts.text.join('\n'), opts.modelOpts, opts.languageIdentifier); model.forceTokenization(model.getLineCount()); let config = new TestConfiguration(opts.editorOpts); let viewModel = new ViewModel(0, config, model, null); @@ -4071,7 +4161,7 @@ suite('autoClosingPairs', () => { test('All cursors should do the same thing when deleting left', () => { let mode = new AutoClosingMode(); - let model = TextModel.createFromString( + let model = createTextModel( [ 'var a = ()' ].join('\n'), @@ -4095,7 +4185,7 @@ suite('autoClosingPairs', () => { }); test('issue #7100: Mouse word selection is strange when non-word character is at the end of line', () => { - let model = TextModel.createFromString( + let model = createTextModel( [ 'before.a', 'before', @@ -4125,7 +4215,7 @@ suite('autoClosingPairs', () => { suite('Undo stops', () => { test('there is an undo stop between typing and deleting left', () => { - let model = TextModel.createFromString( + let model = createTextModel( [ 'A line', 'Another line', @@ -4154,7 +4244,7 @@ suite('Undo stops', () => { }); test('there is an undo stop between typing and deleting right', () => { - let model = TextModel.createFromString( + let model = createTextModel( [ 'A line', 'Another line', @@ -4183,7 +4273,7 @@ suite('Undo stops', () => { }); test('there is an undo stop between deleting left and typing', () => { - let model = TextModel.createFromString( + let model = createTextModel( [ 'A line', 'Another line', @@ -4217,7 +4307,7 @@ suite('Undo stops', () => { }); test('there is an undo stop between deleting left and deleting right', () => { - let model = TextModel.createFromString( + let model = createTextModel( [ 'A line', 'Another line', @@ -4255,7 +4345,7 @@ suite('Undo stops', () => { }); test('there is an undo stop between deleting right and typing', () => { - let model = TextModel.createFromString( + let model = createTextModel( [ 'A line', 'Another line', @@ -4286,7 +4376,7 @@ suite('Undo stops', () => { }); test('there is an undo stop between deleting right and deleting left', () => { - let model = TextModel.createFromString( + let model = createTextModel( [ 'A line', 'Another line', @@ -4322,7 +4412,7 @@ suite('Undo stops', () => { }); test('inserts undo stop when typing space', () => { - let model = TextModel.createFromString( + let model = createTextModel( [ 'A line', 'Another line', diff --git a/src/vs/editor/test/browser/controller/textAreaState.test.ts b/src/vs/editor/test/browser/controller/textAreaState.test.ts index fce77b0211d..492ad210207 100644 --- a/src/vs/editor/test/browser/controller/textAreaState.test.ts +++ b/src/vs/editor/test/browser/controller/textAreaState.test.ts @@ -82,7 +82,7 @@ suite('TextAreaState', () => { textArea._value = 'Hello world!'; textArea._selectionStart = 1; textArea._selectionEnd = 12; - let actual = TextAreaState.EMPTY.readFromTextArea(textArea); + let actual = TextAreaState.readFromTextArea(textArea); assertTextAreaState(actual, 'Hello world!', 1, 12); assert.equal(actual.value, 'Hello world!'); @@ -124,7 +124,7 @@ suite('TextAreaState', () => { textArea.dispose(); }); - function testDeduceInput(prevState: TextAreaState, value: string, selectionStart: number, selectionEnd: number, expected: string, expectedCharReplaceCnt: number): void { + function testDeduceInput(prevState: TextAreaState, value: string, selectionStart: number, selectionEnd: number, couldBeEmojiInput: boolean, couldBeTypingAtOffset0: boolean, expected: string, expectedCharReplaceCnt: number): void { prevState = prevState || TextAreaState.EMPTY; let textArea = new MockTextAreaWrapper(); @@ -132,8 +132,8 @@ suite('TextAreaState', () => { textArea._selectionStart = selectionStart; textArea._selectionEnd = selectionEnd; - let newState = prevState.readFromTextArea(textArea); - let actual = TextAreaState.deduceInput(prevState, newState, true); + let newState = TextAreaState.readFromTextArea(textArea); + let actual = TextAreaState.deduceInput(prevState, newState, couldBeEmojiInput, couldBeTypingAtOffset0); assert.equal(actual.text, expected); assert.equal(actual.replaceCharCnt, expectedCharReplaceCnt); @@ -154,7 +154,7 @@ suite('TextAreaState', () => { testDeduceInput( TextAreaState.EMPTY, 's', - 0, 1, + 0, 1, true, false, 's', 0 ); @@ -164,7 +164,7 @@ suite('TextAreaState', () => { testDeduceInput( new TextAreaState('s', 0, 1, null, null), 'せ', - 0, 1, + 0, 1, true, false, 'せ', 1 ); @@ -174,7 +174,7 @@ suite('TextAreaState', () => { testDeduceInput( new TextAreaState('せ', 0, 1, null, null), 'せn', - 0, 2, + 0, 2, true, false, 'せn', 1 ); @@ -184,7 +184,7 @@ suite('TextAreaState', () => { testDeduceInput( new TextAreaState('せn', 0, 2, null, null), 'せん', - 0, 2, + 0, 2, true, false, 'せん', 2 ); @@ -194,7 +194,7 @@ suite('TextAreaState', () => { testDeduceInput( new TextAreaState('せん', 0, 2, null, null), 'せんs', - 0, 3, + 0, 3, true, false, 'せんs', 2 ); @@ -204,7 +204,7 @@ suite('TextAreaState', () => { testDeduceInput( new TextAreaState('せんs', 0, 3, null, null), 'せんせ', - 0, 3, + 0, 3, true, false, 'せんせ', 3 ); @@ -214,7 +214,7 @@ suite('TextAreaState', () => { testDeduceInput( new TextAreaState('せんせ', 0, 3, null, null), 'せんせ', - 0, 3, + 0, 3, true, false, 'せんせ', 3 ); @@ -224,7 +224,7 @@ suite('TextAreaState', () => { testDeduceInput( new TextAreaState('せんせ', 0, 3, null, null), 'せんせい', - 0, 4, + 0, 4, true, false, 'せんせい', 3 ); @@ -234,7 +234,7 @@ suite('TextAreaState', () => { testDeduceInput( new TextAreaState('せんせい', 0, 4, null, null), 'せんせい', - 4, 4, + 4, 4, true, false, '', 0 ); }); @@ -253,7 +253,7 @@ suite('TextAreaState', () => { testDeduceInput( new TextAreaState('せんせい', 0, 4, null, null), 'せんせい', - 0, 4, + 0, 4, true, false, 'せんせい', 4 ); @@ -263,7 +263,7 @@ suite('TextAreaState', () => { testDeduceInput( new TextAreaState('せんせい', 0, 4, null, null), '先生', - 0, 2, + 0, 2, true, false, '先生', 4 ); @@ -273,7 +273,7 @@ suite('TextAreaState', () => { testDeduceInput( new TextAreaState('先生', 0, 2, null, null), '先生', - 2, 2, + 2, 2, true, false, '', 0 ); }); @@ -282,7 +282,7 @@ suite('TextAreaState', () => { testDeduceInput( null, 'a', - 0, 1, + 0, 1, true, false, 'a', 0 ); }); @@ -291,7 +291,7 @@ suite('TextAreaState', () => { testDeduceInput( new TextAreaState(']\n', 1, 2, null, null), ']\n', - 2, 2, + 2, 2, true, false, '\n', 0 ); }); @@ -300,7 +300,7 @@ suite('TextAreaState', () => { testDeduceInput( null, 'a', - 1, 1, + 1, 1, true, false, 'a', 0 ); }); @@ -309,7 +309,7 @@ suite('TextAreaState', () => { testDeduceInput( TextAreaState.EMPTY, 'a', - 0, 1, + 0, 1, true, false, 'a', 0 ); }); @@ -318,7 +318,7 @@ suite('TextAreaState', () => { testDeduceInput( TextAreaState.EMPTY, 'a', - 1, 1, + 1, 1, true, false, 'a', 0 ); }); @@ -327,7 +327,7 @@ suite('TextAreaState', () => { testDeduceInput( new TextAreaState('Hello world!', 0, 12, null, null), 'H', - 1, 1, + 1, 1, true, false, 'H', 0 ); }); @@ -336,7 +336,7 @@ suite('TextAreaState', () => { testDeduceInput( new TextAreaState('Hello world!', 12, 12, null, null), 'Hello world!a', - 13, 13, + 13, 13, true, false, 'a', 0 ); }); @@ -345,7 +345,7 @@ suite('TextAreaState', () => { testDeduceInput( new TextAreaState('Hello world!', 0, 0, null, null), 'aHello world!', - 1, 1, + 1, 1, true, false, 'a', 0 ); }); @@ -354,7 +354,7 @@ suite('TextAreaState', () => { testDeduceInput( new TextAreaState('Hello world!', 6, 11, null, null), 'Hello other!', - 11, 11, + 11, 11, true, false, 'other', 0 ); }); @@ -363,7 +363,7 @@ suite('TextAreaState', () => { testDeduceInput( TextAreaState.EMPTY, 'これは', - 3, 3, + 3, 3, true, false, 'これは', 0 ); }); @@ -372,7 +372,7 @@ suite('TextAreaState', () => { testDeduceInput( new TextAreaState('Hello world!', 0, 0, null, null), 'Aello world!', - 1, 1, + 1, 1, true, false, 'A', 0 ); }); @@ -381,7 +381,7 @@ suite('TextAreaState', () => { testDeduceInput( new TextAreaState('Hello world!', 5, 5, null, null), 'Hellö world!', - 4, 5, + 4, 5, true, false, 'ö', 0 ); }); @@ -390,7 +390,7 @@ suite('TextAreaState', () => { testDeduceInput( new TextAreaState('Hello world!', 5, 5, null, null), 'Hellöö world!', - 5, 5, + 5, 5, true, false, 'öö', 1 ); }); @@ -399,7 +399,7 @@ suite('TextAreaState', () => { testDeduceInput( new TextAreaState('Hello world!', 5, 5, null, null), 'Helöö world!', - 5, 5, + 5, 5, true, false, 'öö', 2 ); }); @@ -408,7 +408,7 @@ suite('TextAreaState', () => { testDeduceInput( new TextAreaState('Hello world!', 5, 5, null, null), 'Hellö world!', - 5, 5, + 5, 5, true, false, 'ö', 1 ); }); @@ -417,7 +417,7 @@ suite('TextAreaState', () => { testDeduceInput( new TextAreaState('a', 0, 1, null, null), 'a', - 1, 1, + 1, 1, true, false, 'a', 0 ); }); @@ -426,7 +426,7 @@ suite('TextAreaState', () => { testDeduceInput( new TextAreaState('x x', 0, 1, null, null), 'x x', - 1, 1, + 1, 1, true, false, 'x', 0 ); }); @@ -456,7 +456,7 @@ suite('TextAreaState', () => { 'some6 text', 'some7 text' ].join('\n'), - 4, 4, + 4, 4, true, false, '📅', 0 ); }); @@ -470,7 +470,7 @@ suite('TextAreaState', () => { null, null ), 'some💊1 text', - 6, 6, + 6, 6, true, false, '💊', 0 ); }); @@ -484,7 +484,7 @@ suite('TextAreaState', () => { null, null ), 'qwertyu\nasdfghj\nzxcvbnm🎈', - 25, 25, + 25, 25, true, false, '🎈', 0 ); }); @@ -499,11 +499,25 @@ suite('TextAreaState', () => { null, null ), 'some⌨️1 text', - 6, 6, + 6, 6, true, false, '⌨️', 0 ); }); + test('issue #42251: Minor issue, character swapped when typing', () => { + // Typing on OSX occurs at offset 0 after moving the window using the custom (non-native) titlebar. + testDeduceInput( + new TextAreaState( + 'ab', + 2, 2, + null, null + ), + 'cab', + 1, 1, true, true, + 'c', 0 + ); + }); + suite('PagedScreenReaderStrategy', () => { function testPagedScreenReaderStrategy(lines: string[], selection: Selection, expected: TextAreaState): void { diff --git a/src/vs/editor/test/browser/testCodeEditor.ts b/src/vs/editor/test/browser/testCodeEditor.ts index 675277aed4d..0c0759e7a08 100644 --- a/src/vs/editor/test/browser/testCodeEditor.ts +++ b/src/vs/editor/test/browser/testCodeEditor.ts @@ -24,6 +24,8 @@ import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions'; import { TPromise } from 'vs/base/common/winjs.base'; import { onUnexpectedError } from 'vs/base/common/errors'; import { IModelDecorationOptions, ITextModel } from 'vs/editor/common/model'; +import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; +import { INotificationService } from 'vs/platform/notification/common/notification'; export class TestCodeEditor extends CommonCodeEditor implements editorBrowser.ICodeEditor { @@ -168,12 +170,14 @@ export function createTestCodeEditor(model: ITextModel): TestCodeEditor { function _createTestCodeEditor(options: TestCodeEditorCreationOptions): TestCodeEditor { let contextKeyService = new MockContextKeyService(); + let notificationService = new TestNotificationService(); let services = options.serviceCollection || new ServiceCollection(); services.set(IContextKeyService, contextKeyService); + services.set(INotificationService, notificationService); let instantiationService = new InstantiationService(services); - let editor = new TestCodeEditor(new MockScopeLocation(), options, false, instantiationService, contextKeyService); + let editor = new TestCodeEditor(new MockScopeLocation(), options, false, instantiationService, contextKeyService, notificationService); editor.setModel(options.model); return editor; } diff --git a/src/vs/editor/test/common/config/commonEditorConfig.test.ts b/src/vs/editor/test/common/config/commonEditorConfig.test.ts index 4d5da6aec32..ef16d492ef5 100644 --- a/src/vs/editor/test/common/config/commonEditorConfig.test.ts +++ b/src/vs/editor/test/common/config/commonEditorConfig.test.ts @@ -88,14 +88,14 @@ suite('Common Editor Config', () => { let config = new TestWrappingConfiguration({ wordWrap: true }); - assertWrapping(config, true, 81); + assertWrapping(config, true, 80); }); test('wordWrap on', () => { let config = new TestWrappingConfiguration({ wordWrap: 'on' }); - assertWrapping(config, true, 81); + assertWrapping(config, true, 80); }); test('wordWrap on without minimap', () => { @@ -105,7 +105,7 @@ suite('Common Editor Config', () => { enabled: false } }); - assertWrapping(config, true, 89); + assertWrapping(config, true, 88); }); test('wordWrap on does not use wordWrapColumn', () => { @@ -113,7 +113,7 @@ suite('Common Editor Config', () => { wordWrap: 'on', wordWrapColumn: 10 }); - assertWrapping(config, true, 81); + assertWrapping(config, true, 80); }); test('wordWrap off', () => { diff --git a/src/vs/editor/test/common/diff/diffComputer.test.ts b/src/vs/editor/test/common/diff/diffComputer.test.ts index 0d521de7eb8..dd513f43452 100644 --- a/src/vs/editor/test/common/diff/diffComputer.test.ts +++ b/src/vs/editor/test/common/diff/diffComputer.test.ts @@ -673,4 +673,23 @@ suite('Editor Diff - DiffComputer', () => { ]; assertDiff(original, modified, expected, true, false); }); + + test('issue #43922', () => { + let original = [ + ' * `yarn [install]` -- Install project NPM dependencies. This is automatically done when you first create the project. You should only need to run this if you add dependencies in `package.json`.', + ]; + let modified = [ + ' * `yarn` -- Install project NPM dependencies. You should only need to run this if you add dependencies in `package.json`.', + ]; + var expected = [ + createLineChange( + 1, 1, 1, 1, + [ + createCharChange(1, 9, 1, 19, 0, 0, 0, 0), + createCharChange(1, 58, 1, 120, 0, 0, 0, 0), + ] + ) + ]; + assertDiff(original, modified, expected, true, false); + }); }); diff --git a/src/vs/editor/test/common/editorTestUtils.ts b/src/vs/editor/test/common/editorTestUtils.ts index cdc1b3f2aca..d2e8cb2072e 100644 --- a/src/vs/editor/test/common/editorTestUtils.ts +++ b/src/vs/editor/test/common/editorTestUtils.ts @@ -5,9 +5,37 @@ 'use strict'; import { TextModel } from 'vs/editor/common/model/textModel'; +import { DefaultEndOfLine, ITextModelCreationOptions } from 'vs/editor/common/model'; +import { LanguageIdentifier } from 'vs/editor/common/modes'; +import URI from 'vs/base/common/uri'; export function withEditorModel(text: string[], callback: (model: TextModel) => void): void { var model = TextModel.createFromString(text.join('\n')); callback(model); model.dispose(); } + +export interface IRelaxedTextModelCreationOptions { + tabSize?: number; + insertSpaces?: boolean; + detectIndentation?: boolean; + trimAutoWhitespace?: boolean; + defaultEOL?: DefaultEndOfLine; + isForSimpleWidget?: boolean; + largeFileSize?: number; + largeFileLineCount?: number; +} + +export function createTextModel(text: string, _options: IRelaxedTextModelCreationOptions = TextModel.DEFAULT_CREATION_OPTIONS, languageIdentifier: LanguageIdentifier = null, uri: URI = null): TextModel { + const options: ITextModelCreationOptions = { + tabSize: (typeof _options.tabSize === 'undefined' ? TextModel.DEFAULT_CREATION_OPTIONS.tabSize : _options.tabSize), + insertSpaces: (typeof _options.insertSpaces === 'undefined' ? TextModel.DEFAULT_CREATION_OPTIONS.insertSpaces : _options.insertSpaces), + detectIndentation: (typeof _options.detectIndentation === 'undefined' ? TextModel.DEFAULT_CREATION_OPTIONS.detectIndentation : _options.detectIndentation), + trimAutoWhitespace: (typeof _options.trimAutoWhitespace === 'undefined' ? TextModel.DEFAULT_CREATION_OPTIONS.trimAutoWhitespace : _options.trimAutoWhitespace), + defaultEOL: (typeof _options.defaultEOL === 'undefined' ? TextModel.DEFAULT_CREATION_OPTIONS.defaultEOL : _options.defaultEOL), + isForSimpleWidget: (typeof _options.isForSimpleWidget === 'undefined' ? TextModel.DEFAULT_CREATION_OPTIONS.isForSimpleWidget : _options.isForSimpleWidget), + largeFileSize: (typeof _options.largeFileSize === 'undefined' ? TextModel.DEFAULT_CREATION_OPTIONS.largeFileSize : _options.largeFileSize), + largeFileLineCount: (typeof _options.largeFileLineCount === 'undefined' ? TextModel.DEFAULT_CREATION_OPTIONS.largeFileLineCount : _options.largeFileLineCount), + }; + return TextModel.createFromString(text, options, languageIdentifier, uri); +} diff --git a/src/vs/editor/test/common/model/benchmark/benchmarkUtils.ts b/src/vs/editor/test/common/model/benchmark/benchmarkUtils.ts index f466e4d21c8..42f00825eac 100644 --- a/src/vs/editor/test/common/model/benchmark/benchmarkUtils.ts +++ b/src/vs/editor/test/common/model/benchmark/benchmarkUtils.ts @@ -4,9 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { ITextBufferBuilder, ITextBufferFactory, ITextBuffer, DefaultEndOfLine } from 'vs/editor/common/model'; -import { LinesTextBufferBuilder } from 'vs/editor/common/model/linesTextBuffer/linesTextBufferBuilder'; import { PieceTreeTextBufferBuilder } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBufferBuilder'; -import { ChunksTextBufferBuilder } from 'vs/editor/common/model/chunksTextBuffer/chunksTextBufferBuilder'; export function doBenchmark(id: string, ts: T[], fn: (t: T) => void) { let columns: string[] = [id]; @@ -57,7 +55,7 @@ export class BenchmarkSuite { for (let i = 0; i < this.benchmarks.length; i++) { let benchmark = this.benchmarks[i]; let columns: string[] = [benchmark.name]; - [new LinesTextBufferBuilder(), new PieceTreeTextBufferBuilder(), new ChunksTextBufferBuilder()].forEach((builder: ITextBufferBuilder) => { + [new PieceTreeTextBufferBuilder()].forEach((builder: ITextBufferBuilder) => { let timeDiffTotal = 0.0; for (let j = 0; j < this.iterations; j++) { let factory = benchmark.buildBuffer(builder); diff --git a/src/vs/editor/test/common/model/benchmark/modelbuilder.benchmark.ts b/src/vs/editor/test/common/model/benchmark/modelbuilder.benchmark.ts index 41445a17df9..da57b7089d7 100644 --- a/src/vs/editor/test/common/model/benchmark/modelbuilder.benchmark.ts +++ b/src/vs/editor/test/common/model/benchmark/modelbuilder.benchmark.ts @@ -4,13 +4,11 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import { LinesTextBufferBuilder } from 'vs/editor/common/model/linesTextBuffer/linesTextBufferBuilder'; import { PieceTreeTextBufferBuilder } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBufferBuilder'; import { ITextBufferBuilder } from 'vs/editor/common/model'; import { generateRandomChunkWithLF } from 'vs/editor/test/common/model/linesTextBuffer/textBufferAutoTestUtils'; import { doBenchmark } from 'vs/editor/test/common/model/benchmark/benchmarkUtils'; -let linesTextBufferBuilder = new LinesTextBufferBuilder(); let pieceTreeTextBufferBuilder = new PieceTreeTextBufferBuilder(); let chunks = []; @@ -30,5 +28,5 @@ let modelBuildBenchmark = function (id: string, builders: ITextBufferBuilder[], console.log(`|model builder\t|line buffer\t|piece table\t|`); console.log('|---|---|---|'); for (let i of [10, 100]) { - modelBuildBenchmark(`${i} random chunks`, [linesTextBufferBuilder, pieceTreeTextBufferBuilder], i); + modelBuildBenchmark(`${i} random chunks`, [pieceTreeTextBufferBuilder], i); } diff --git a/src/vs/editor/test/common/model/chunksTextBuffer/bufferPiece.test.ts b/src/vs/editor/test/common/model/chunksTextBuffer/bufferPiece.test.ts deleted file mode 100644 index a6cc8f6f22d..00000000000 --- a/src/vs/editor/test/common/model/chunksTextBuffer/bufferPiece.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; - -import * as assert from 'assert'; -import { BufferPiece } from 'vs/editor/common/model/chunksTextBuffer/bufferPiece'; - -suite('BufferPiece', () => { - test('findLineStartBeforeOffset', () => { - let piece = new BufferPiece([ - 'Line1\r\n', - 'l2\n', - 'another\r', - 'and\r\n', - 'finally\n', - 'last' - ].join('')); - - assert.equal(piece.length(), 35); - assert.deepEqual(piece.findLineStartBeforeOffset(0), -1); - assert.deepEqual(piece.findLineStartBeforeOffset(1), -1); - assert.deepEqual(piece.findLineStartBeforeOffset(2), -1); - assert.deepEqual(piece.findLineStartBeforeOffset(3), -1); - assert.deepEqual(piece.findLineStartBeforeOffset(4), -1); - assert.deepEqual(piece.findLineStartBeforeOffset(5), -1); - assert.deepEqual(piece.findLineStartBeforeOffset(6), -1); - assert.deepEqual(piece.findLineStartBeforeOffset(7), 0); - assert.deepEqual(piece.findLineStartBeforeOffset(8), 0); - assert.deepEqual(piece.findLineStartBeforeOffset(9), 0); - assert.deepEqual(piece.findLineStartBeforeOffset(10), 1); - assert.deepEqual(piece.findLineStartBeforeOffset(11), 1); - assert.deepEqual(piece.findLineStartBeforeOffset(12), 1); - assert.deepEqual(piece.findLineStartBeforeOffset(13), 1); - assert.deepEqual(piece.findLineStartBeforeOffset(14), 1); - assert.deepEqual(piece.findLineStartBeforeOffset(15), 1); - assert.deepEqual(piece.findLineStartBeforeOffset(16), 1); - assert.deepEqual(piece.findLineStartBeforeOffset(17), 1); - assert.deepEqual(piece.findLineStartBeforeOffset(18), 2); - assert.deepEqual(piece.findLineStartBeforeOffset(19), 2); - assert.deepEqual(piece.findLineStartBeforeOffset(20), 2); - assert.deepEqual(piece.findLineStartBeforeOffset(21), 2); - assert.deepEqual(piece.findLineStartBeforeOffset(22), 2); - assert.deepEqual(piece.findLineStartBeforeOffset(23), 3); - assert.deepEqual(piece.findLineStartBeforeOffset(24), 3); - assert.deepEqual(piece.findLineStartBeforeOffset(25), 3); - assert.deepEqual(piece.findLineStartBeforeOffset(26), 3); - assert.deepEqual(piece.findLineStartBeforeOffset(27), 3); - assert.deepEqual(piece.findLineStartBeforeOffset(28), 3); - assert.deepEqual(piece.findLineStartBeforeOffset(29), 3); - assert.deepEqual(piece.findLineStartBeforeOffset(30), 3); - assert.deepEqual(piece.findLineStartBeforeOffset(31), 4); - assert.deepEqual(piece.findLineStartBeforeOffset(32), 4); - assert.deepEqual(piece.findLineStartBeforeOffset(33), 4); - assert.deepEqual(piece.findLineStartBeforeOffset(34), 4); - assert.deepEqual(piece.findLineStartBeforeOffset(35), 4); - assert.deepEqual(piece.findLineStartBeforeOffset(36), 4); - }); -}); diff --git a/src/vs/editor/test/common/model/editableTextModel.test.ts b/src/vs/editor/test/common/model/editableTextModel.test.ts index b2ce9f1c5da..fa41d0d5f49 100644 --- a/src/vs/editor/test/common/model/editableTextModel.test.ts +++ b/src/vs/editor/test/common/model/editableTextModel.test.ts @@ -1069,4 +1069,26 @@ suite('EditorModel - EditableTextModel.applyEdits', () => { model.dispose(); mirrorModel2.dispose(); }); + + test('issue #47733: Undo mangles unicode characters', () => { + let model = createEditableTextModelFromString('\'👁\''); + + model.applyEdits([ + { range: new Range(1, 1, 1, 1), text: '"' }, + { range: new Range(1, 2, 1, 2), text: '"' }, + ]); + + // assert.equal(model.getValue(EndOfLinePreference.LF), '"\'"👁\''); + + assert.deepEqual(model.validateRange(new Range(1, 3, 1, 4)), new Range(1, 3, 1, 4)); + + // model.applyEdits([ + // { range: new Range(1, 1, 1, 2), text: null }, + // { range: new Range(1, 3, 1, 4), text: null }, + // ]); + + // assert.equal(model.getValue(EndOfLinePreference.LF), '\'👁\''); + + model.dispose(); + }); }); diff --git a/src/vs/editor/test/common/model/editableTextModelTestUtils.ts b/src/vs/editor/test/common/model/editableTextModelTestUtils.ts index 7f1fc466033..035ec1a9221 100644 --- a/src/vs/editor/test/common/model/editableTextModelTestUtils.ts +++ b/src/vs/editor/test/common/model/editableTextModelTestUtils.ts @@ -31,8 +31,17 @@ export function testApplyEditsWithSyncedModels(original: string[], edits: IIdent assert.deepEqual(model.getValue(EndOfLinePreference.LF), originalStr); if (!inputEditsAreInvalid) { + const simplifyEdit = (edit: IIdentifiedSingleEditOperation) => { + return { + identifier: edit.identifier, + range: edit.range, + text: edit.text, + forceMoveMarkers: edit.forceMoveMarkers, + isAutoWhitespaceEdit: edit.isAutoWhitespaceEdit + }; + }; // Assert the inverse of the inverse edits are the original edits - assert.deepEqual(inverseInverseEdits, edits); + assert.deepEqual(inverseInverseEdits.map(simplifyEdit), edits.map(simplifyEdit)); } assertMirrorModels(); diff --git a/src/vs/editor/test/common/model/linesTextBuffer/linesTextBuffer.test.ts b/src/vs/editor/test/common/model/linesTextBuffer/linesTextBuffer.test.ts index 155bac69a70..3f14797774a 100644 --- a/src/vs/editor/test/common/model/linesTextBuffer/linesTextBuffer.test.ts +++ b/src/vs/editor/test/common/model/linesTextBuffer/linesTextBuffer.test.ts @@ -7,9 +7,11 @@ import * as assert from 'assert'; import { Range } from 'vs/editor/common/core/range'; -import { LinesTextBuffer, IValidatedEditOperation } from 'vs/editor/common/model/linesTextBuffer/linesTextBuffer'; +import { PieceTreeTextBuffer, IValidatedEditOperation } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer'; +import { createTextBufferFactory } from 'vs/editor/common/model/textModel'; +import { DefaultEndOfLine } from 'vs/editor/common/model'; -suite('LinesTextBuffer._getInverseEdits', () => { +suite('PieceTreeTextBuffer._getInverseEdits', () => { function editOp(startLineNumber: number, startColumn: number, endLineNumber: number, endColumn: number, text: string[]): IValidatedEditOperation { return { @@ -29,7 +31,7 @@ suite('LinesTextBuffer._getInverseEdits', () => { } function assertInverseEdits(ops: IValidatedEditOperation[], expected: Range[]): void { - var actual = LinesTextBuffer._getInverseEditRanges(ops); + var actual = PieceTreeTextBuffer._getInverseEditRanges(ops); assert.deepEqual(actual, expected); } @@ -260,7 +262,7 @@ suite('LinesTextBuffer._getInverseEdits', () => { }); }); -suite('LinesTextBuffer._toSingleEditOperation', () => { +suite('PieceTreeTextBuffer._toSingleEditOperation', () => { function editOp(startLineNumber: number, startColumn: number, endLineNumber: number, endColumn: number, rangeOffset: number, rangeLength: number, text: string[]): IValidatedEditOperation { return { @@ -276,13 +278,7 @@ suite('LinesTextBuffer._toSingleEditOperation', () => { } function testToSingleEditOperation(original: string[], edits: IValidatedEditOperation[], expected: IValidatedEditOperation): void { - const textBuffer = new LinesTextBuffer({ - BOM: '', - EOL: '\n', - containsRTL: false, - isBasicASCII: true, - lines: original - }); + const textBuffer = createTextBufferFactory(original.join('\n')).create(DefaultEndOfLine.LF); const actual = textBuffer._toSingleEditOperation(edits); assert.deepEqual(actual, expected); diff --git a/src/vs/editor/test/common/model/linesTextBuffer/linesTextBufferBuilder.test.ts b/src/vs/editor/test/common/model/linesTextBuffer/linesTextBufferBuilder.test.ts index afae933900b..0291217b61d 100644 --- a/src/vs/editor/test/common/model/linesTextBuffer/linesTextBufferBuilder.test.ts +++ b/src/vs/editor/test/common/model/linesTextBuffer/linesTextBufferBuilder.test.ts @@ -5,150 +5,69 @@ 'use strict'; import * as assert from 'assert'; -import { LinesTextBufferBuilder } from 'vs/editor/common/model/linesTextBuffer/linesTextBufferBuilder'; -import { ITextModelCreationOptions } from 'vs/editor/common/model'; -import { TextModel } from 'vs/editor/common/model/textModel'; +import { DefaultEndOfLine } from 'vs/editor/common/model'; +import { createTextBufferFactory } from 'vs/editor/common/model/textModel'; import * as strings from 'vs/base/common/strings'; -import { IRawTextSource } from 'vs/editor/common/model/linesTextBuffer/textSource'; +import { PieceTreeTextBuffer } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer'; -class RawTextSource { - public static fromString(rawText: string): IRawTextSource { - // Count the number of lines that end with \r\n - let carriageReturnCnt = 0; - let lastCarriageReturnIndex = -1; - while ((lastCarriageReturnIndex = rawText.indexOf('\r', lastCarriageReturnIndex + 1)) !== -1) { - carriageReturnCnt++; - } +export function testTextBufferFactory(text: string, eol: string, mightContainNonBasicASCII: boolean, mightContainRTL: boolean): void { + const textBuffer = createTextBufferFactory(text).create(DefaultEndOfLine.LF); - const containsRTL = strings.containsRTL(rawText); - const isBasicASCII = (containsRTL ? false : strings.isBasicASCII(rawText)); - - // Split the text into lines - const lines = rawText.split(/\r\n|\r|\n/); - - // Remove the BOM (if present) - let BOM = ''; - if (strings.startsWithUTF8BOM(lines[0])) { - BOM = strings.UTF8_BOM_CHARACTER; - lines[0] = lines[0].substr(1); - } - - return { - BOM: BOM, - lines: lines, - containsRTL: containsRTL, - isBasicASCII: isBasicASCII, - totalCRCount: carriageReturnCnt - }; - } -} - -export function testModelBuilder(chunks: string[], opts: ITextModelCreationOptions = TextModel.DEFAULT_CREATION_OPTIONS): void { - let expectedTextSource = RawTextSource.fromString(chunks.join('')); - - let builder = new LinesTextBufferBuilder(); - for (let i = 0, len = chunks.length; i < len; i++) { - builder.acceptChunk(chunks[i]); - } - let actual = builder.finish(); - - assert.deepEqual(actual.rawTextSource, expectedTextSource); + assert.equal(textBuffer.mightContainNonBasicASCII(), mightContainNonBasicASCII); + assert.equal(textBuffer.mightContainRTL(), mightContainRTL); + assert.equal(textBuffer.getEOL(), eol); } suite('ModelBuilder', () => { - test('no chunks', () => { - testModelBuilder([]); + test('t1', () => { + testTextBufferFactory('', '\n', false, false); }); - test('single empty chunk', () => { - testModelBuilder(['']); + test('t2', () => { + testTextBufferFactory('Hello world', '\n', false, false); }); - test('single line in one chunk', () => { - testModelBuilder(['Hello world']); + test('t3', () => { + testTextBufferFactory('Hello world\nHow are you?', '\n', false, false); }); - test('single line in multiple chunks', () => { - testModelBuilder(['Hello', ' ', 'world']); - }); - - test('two lines in single chunk', () => { - testModelBuilder(['Hello world\nHow are you?']); - }); - - test('two lines in multiple chunks 1', () => { - testModelBuilder(['Hello worl', 'd\nHow are you?']); - }); - - test('two lines in multiple chunks 2', () => { - testModelBuilder(['Hello worl', 'd', '\n', 'H', 'ow are you?']); - }); - - test('two lines in multiple chunks 3', () => { - testModelBuilder(['Hello worl', 'd', '\nHow are you?']); - }); - - test('multiple lines in single chunks', () => { - testModelBuilder(['Hello world\nHow are you?\nIs everything good today?\nDo you enjoy the weather?']); - }); - - test('multiple lines in multiple chunks 1', () => { - testModelBuilder(['Hello world\nHow are you', '?\nIs everything good today?\nDo you enjoy the weather?']); - }); - - test('multiple lines in multiple chunks 1', () => { - testModelBuilder(['Hello world', '\nHow are you', '?\nIs everything good today?', '\nDo you enjoy the weather?']); - }); - - test('multiple lines in multiple chunks 1', () => { - testModelBuilder(['Hello world\n', 'How are you', '?\nIs everything good today?', '\nDo you enjoy the weather?']); + test('t4', () => { + testTextBufferFactory('Hello world\nHow are you?\nIs everything good today?\nDo you enjoy the weather?', '\n', false, false); }); test('carriage return detection (1 \\r\\n 2 \\n)', () => { - testModelBuilder(['Hello world\r\n', 'How are you', '?\nIs everything good today?', '\nDo you enjoy the weather?']); + testTextBufferFactory('Hello world\r\nHow are you?\nIs everything good today?\nDo you enjoy the weather?', '\n', false, false); }); test('carriage return detection (2 \\r\\n 1 \\n)', () => { - testModelBuilder(['Hello world\r\n', 'How are you', '?\r\nIs everything good today?', '\nDo you enjoy the weather?']); + testTextBufferFactory('Hello world\r\nHow are you?\r\nIs everything good today?\nDo you enjoy the weather?', '\r\n', false, false); }); test('carriage return detection (3 \\r\\n 0 \\n)', () => { - testModelBuilder(['Hello world\r\n', 'How are you', '?\r\nIs everything good today?', '\r\nDo you enjoy the weather?']); - }); - - test('carriage return detection (isolated \\r)', () => { - testModelBuilder(['Hello world', '\r', '\n', 'How are you', '?', '\r', '\n', 'Is everything good today?', '\r', '\n', 'Do you enjoy the weather?']); + testTextBufferFactory('Hello world\r\nHow are you?\r\nIs everything good today?\r\nDo you enjoy the weather?', '\r\n', false, false); }); test('BOM handling', () => { - testModelBuilder([strings.UTF8_BOM_CHARACTER + 'Hello world!']); + testTextBufferFactory(strings.UTF8_BOM_CHARACTER + 'Hello world!', '\n', false, false); }); test('BOM handling', () => { - testModelBuilder([strings.UTF8_BOM_CHARACTER, 'Hello world!']); - }); - - test('RTL handling 1', () => { - testModelBuilder(['Hello world!', 'זוהי עובדה מבוססת שדעתו']); + testTextBufferFactory(strings.UTF8_BOM_CHARACTER + 'Hello world!', '\n', false, false); }); test('RTL handling 2', () => { - testModelBuilder(['Hello world!זוהי עובדה מבוססת שדעתו']); + testTextBufferFactory('Hello world!זוהי עובדה מבוססת שדעתו', '\n', true, true); }); test('RTL handling 3', () => { - testModelBuilder(['Hello world!זוהי \nעובדה מבוססת שדעתו']); + testTextBufferFactory('Hello world!זוהי \nעובדה מבוססת שדעתו', '\n', true, true); }); test('ASCII handling 1', () => { - testModelBuilder(['Hello world!!\nHow do you do?']); + testTextBufferFactory('Hello world!!\nHow do you do?', '\n', false, false); }); - test('ASCII handling 1', () => { - testModelBuilder(['Hello world!!\nHow do you do?Züricha📚📚b']); - }); - - test('issue #32819: some special string cannot be displayed completely', () => { - testModelBuilder(['AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA123']); + test('ASCII handling 2', () => { + testTextBufferFactory('Hello world!!\nHow do you do?Züricha📚📚b', '\n', true, false); }); }); diff --git a/src/vs/editor/test/common/model/linesTextBuffer/linesTextBufferBuilderAuto.test.ts b/src/vs/editor/test/common/model/linesTextBuffer/linesTextBufferBuilderAuto.test.ts deleted file mode 100644 index 8d7119d2077..00000000000 --- a/src/vs/editor/test/common/model/linesTextBuffer/linesTextBufferBuilderAuto.test.ts +++ /dev/null @@ -1,97 +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 { testModelBuilder } from './linesTextBufferBuilder.test'; -import { getRandomInt, getRandomEOLSequence, getRandomString } from 'vs/editor/test/common/model/linesTextBuffer/textBufferAutoTestUtils'; - -const GENERATE_TESTS = false; - -suite('ModelBuilder Auto Tests', () => { - - test('auto1', () => { - testModelBuilder(['sarjniow', '\r', '\nbpb', 'ofb', '\njzldgxx', '\r\nkzwfjysng']); - }); - - test('auto2', () => { - testModelBuilder(['i', 'yyernubi\r\niimgn\n', 'ut\r']); - }); - -}); - -function generateRandomFile(): string { - let lineCount = getRandomInt(1, 10); - let mixedEOLSequence = getRandomInt(1, 2) === 1 ? true : false; - let fixedEOL = getRandomEOLSequence(); - let lines: string[] = []; - for (let i = 0; i < lineCount; i++) { - if (i !== 0) { - if (mixedEOLSequence) { - lines.push(getRandomEOLSequence()); - } else { - lines.push(fixedEOL); - } - } - lines.push(getRandomString(0, 10)); - - } - return lines.join(''); -} - -function generateRandomChunks(file: string): string[] { - let result: string[] = []; - let cnt = getRandomInt(1, 20); - - let maxOffset = file.length; - - while (cnt > 0 && maxOffset > 0) { - - let offset = getRandomInt(0, maxOffset); - result.unshift(file.substring(offset, maxOffset)); - // let length = getRandomInt(0, maxOffset - offset); - // let text = generateFile(true); - - // result.push({ - // offset: offset, - // length: length, - // text: text - // }); - - maxOffset = offset; - cnt--; - } - if (maxOffset !== 0) { - result.unshift(file.substring(0, maxOffset)); - } - return result; -} - -function testRandomFile(file: string): boolean { - let tests = getRandomInt(5, 10); - for (let i = 0; i < tests; i++) { - let chunks = generateRandomChunks(file); - try { - testModelBuilder(chunks); - } catch (err) { - console.log(err); - console.log(JSON.stringify(chunks)); - return false; - } - } - return true; -} - -if (GENERATE_TESTS) { - let number = 1; - while (true) { - console.log('------BEGIN NEW TEST: ' + number); - - if (!testRandomFile(generateRandomFile())) { - break; - } - - console.log('------END NEW TEST: ' + (number++)); - } -} diff --git a/src/vs/editor/test/common/model/model.test.ts b/src/vs/editor/test/common/model/model.test.ts index 0c15f9753f5..b53ce735bfc 100644 --- a/src/vs/editor/test/common/model/model.test.ts +++ b/src/vs/editor/test/common/model/model.test.ts @@ -109,7 +109,7 @@ suite('Editor Model - Model', () => { let e: ModelRawContentChangedEvent = null; thisModel.onDidChangeRawContent((_e) => { if (e !== null) { - assert.fail(); + assert.fail('Unexpected assertion error'); } e = _e; }); @@ -128,7 +128,7 @@ suite('Editor Model - Model', () => { let e: ModelRawContentChangedEvent = null; thisModel.onDidChangeRawContent((_e) => { if (e !== null) { - assert.fail(); + assert.fail('Unexpected assertion error'); } e = _e; }); @@ -205,7 +205,7 @@ suite('Editor Model - Model', () => { let e: ModelRawContentChangedEvent = null; thisModel.onDidChangeRawContent((_e) => { if (e !== null) { - assert.fail(); + assert.fail('Unexpected assertion error'); } e = _e; }); @@ -224,7 +224,7 @@ suite('Editor Model - Model', () => { let e: ModelRawContentChangedEvent = null; thisModel.onDidChangeRawContent((_e) => { if (e !== null) { - assert.fail(); + assert.fail('Unexpected assertion error'); } e = _e; }); @@ -243,7 +243,7 @@ suite('Editor Model - Model', () => { let e: ModelRawContentChangedEvent = null; thisModel.onDidChangeRawContent((_e) => { if (e !== null) { - assert.fail(); + assert.fail('Unexpected assertion error'); } e = _e; }); @@ -263,7 +263,7 @@ suite('Editor Model - Model', () => { let e: ModelRawContentChangedEvent = null; thisModel.onDidChangeRawContent((_e) => { if (e !== null) { - assert.fail(); + assert.fail('Unexpected assertion error'); } e = _e; }); @@ -314,7 +314,7 @@ suite('Editor Model - Model', () => { let e: ModelRawContentChangedEvent = null; thisModel.onDidChangeRawContent((_e) => { if (e !== null) { - assert.fail(); + assert.fail('Unexpected assertion error'); } e = _e; }); @@ -328,6 +328,16 @@ suite('Editor Model - Model', () => { false )); }); + + test('issue #46342: Maintain edit operation order in applyEdits', () => { + let res = thisModel.applyEdits([ + { range: new Range(2, 1, 2, 1), text: 'a' }, + { range: new Range(1, 1, 1, 1), text: 'b' }, + ]); + + assert.deepEqual(res[0].range, new Range(2, 1, 2, 2)); + assert.deepEqual(res[1].range, new Range(1, 1, 1, 2)); + }); }); diff --git a/src/vs/editor/test/common/model/modelEditOperation.test.ts b/src/vs/editor/test/common/model/modelEditOperation.test.ts index ac20a1b71d5..e249ec82363 100644 --- a/src/vs/editor/test/common/model/modelEditOperation.test.ts +++ b/src/vs/editor/test/common/model/modelEditOperation.test.ts @@ -68,7 +68,16 @@ suite('Editor Model - Model Edit Operation', () => { assert.equal(model.getLineContent(4), LINE4); assert.equal(model.getLineContent(5), LINE5); - assert.deepEqual(originalOp, editOp); + const simplifyEdit = (edit: IIdentifiedSingleEditOperation) => { + return { + identifier: edit.identifier, + range: edit.range, + text: edit.text, + forceMoveMarkers: edit.forceMoveMarkers, + isAutoWhitespaceEdit: edit.isAutoWhitespaceEdit + }; + }; + assert.deepEqual(originalOp.map(simplifyEdit), editOp.map(simplifyEdit)); } test('Insert inline', () => { diff --git a/src/vs/editor/test/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.test.ts b/src/vs/editor/test/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.test.ts index e5b6970d888..089c17be083 100644 --- a/src/vs/editor/test/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.test.ts +++ b/src/vs/editor/test/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.test.ts @@ -1445,11 +1445,44 @@ suite('centralized lineStarts with CRLF', () => { }); suite('random is unsupervised', () => { + test('splitting large change buffer', function () { + let pieceTable = createTextBuffer([''], false); + let str = ''; + + pieceTable.insert(0, 'WUZ\nXVZY\n'); + str = str.substring(0, 0) + 'WUZ\nXVZY\n' + str.substring(0); + pieceTable.insert(8, '\r\r\nZXUWVW'); + str = str.substring(0, 8) + '\r\r\nZXUWVW' + str.substring(8); + pieceTable.delete(10, 7); + str = str.substring(0, 10) + str.substring(10 + 7); + pieceTable.delete(10, 1); + str = str.substring(0, 10) + str.substring(10 + 1); + pieceTable.insert(4, 'VX\r\r\nWZVZ'); + str = str.substring(0, 4) + 'VX\r\r\nWZVZ' + str.substring(4); + pieceTable.delete(11, 3); + str = str.substring(0, 11) + str.substring(11 + 3); + pieceTable.delete(12, 4); + str = str.substring(0, 12) + str.substring(12 + 4); + pieceTable.delete(8, 0); + str = str.substring(0, 8) + str.substring(8 + 0); + pieceTable.delete(10, 2); + str = str.substring(0, 10) + str.substring(10 + 2); + pieceTable.insert(0, 'VZXXZYZX\r'); + str = str.substring(0, 0) + 'VZXXZYZX\r' + str.substring(0); + + assert.equal(pieceTable.getLinesRawContent(), str); + + testLineStarts(str, pieceTable); + testLinesContent(str, pieceTable); + assertTreeInvariants(pieceTable); + }); + test('random insert delete', function () { this.timeout(500000); let str = ''; let pieceTable = createTextBuffer([str], false); + // let output = ''; for (let i = 0; i < 1000; i++) { if (Math.random() < 0.6) { // insert @@ -1457,6 +1490,8 @@ suite('random is unsupervised', () => { let pos = randomInt(str.length + 1); pieceTable.insert(pos, text); str = str.substring(0, pos) + text + str.substring(pos); + // output += `pieceTable.insert(${pos}, '${text.replace(/\n/g, '\\n').replace(/\r/g, '\\r')}');\n`; + // output += `str = str.substring(0, ${pos}) + '${text.replace(/\n/g, '\\n').replace(/\r/g, '\\r')}' + str.substring(${pos});\n`; } else { // delete let pos = randomInt(str.length); @@ -1466,8 +1501,12 @@ suite('random is unsupervised', () => { ); pieceTable.delete(pos, length); str = str.substring(0, pos) + str.substring(pos + length); + // output += `pieceTable.delete(${pos}, ${length});\n`; + // output += `str = str.substring(0, ${pos}) + str.substring(${pos} + ${length});\n` + } } + // console.log(output); assert.equal(pieceTable.getLinesRawContent(), str); @@ -1571,6 +1610,37 @@ suite('buffer api', () => { assert(!a.equal(b)); }); + + test('getLineCharCode - issue #45735', () => { + let pieceTable = createTextBuffer(['LINE1\nline2']); + assert.equal(pieceTable.getLineCharCode(1, 0), 'L'.charCodeAt(0), 'L'); + assert.equal(pieceTable.getLineCharCode(1, 1), 'I'.charCodeAt(0), 'I'); + assert.equal(pieceTable.getLineCharCode(1, 2), 'N'.charCodeAt(0), 'N'); + assert.equal(pieceTable.getLineCharCode(1, 3), 'E'.charCodeAt(0), 'E'); + assert.equal(pieceTable.getLineCharCode(1, 4), '1'.charCodeAt(0), '1'); + assert.equal(pieceTable.getLineCharCode(1, 5), '\n'.charCodeAt(0), '\\n'); + assert.equal(pieceTable.getLineCharCode(2, 0), 'l'.charCodeAt(0), 'l'); + assert.equal(pieceTable.getLineCharCode(2, 1), 'i'.charCodeAt(0), 'i'); + assert.equal(pieceTable.getLineCharCode(2, 2), 'n'.charCodeAt(0), 'n'); + assert.equal(pieceTable.getLineCharCode(2, 3), 'e'.charCodeAt(0), 'e'); + assert.equal(pieceTable.getLineCharCode(2, 4), '2'.charCodeAt(0), '2'); + }); + + + test('getLineCharCode - issue #47733', () => { + let pieceTable = createTextBuffer(['', 'LINE1\n', 'line2']); + assert.equal(pieceTable.getLineCharCode(1, 0), 'L'.charCodeAt(0), 'L'); + assert.equal(pieceTable.getLineCharCode(1, 1), 'I'.charCodeAt(0), 'I'); + assert.equal(pieceTable.getLineCharCode(1, 2), 'N'.charCodeAt(0), 'N'); + assert.equal(pieceTable.getLineCharCode(1, 3), 'E'.charCodeAt(0), 'E'); + assert.equal(pieceTable.getLineCharCode(1, 4), '1'.charCodeAt(0), '1'); + assert.equal(pieceTable.getLineCharCode(1, 5), '\n'.charCodeAt(0), '\\n'); + assert.equal(pieceTable.getLineCharCode(2, 0), 'l'.charCodeAt(0), 'l'); + assert.equal(pieceTable.getLineCharCode(2, 1), 'i'.charCodeAt(0), 'i'); + assert.equal(pieceTable.getLineCharCode(2, 2), 'n'.charCodeAt(0), 'n'); + assert.equal(pieceTable.getLineCharCode(2, 3), 'e'.charCodeAt(0), 'e'); + assert.equal(pieceTable.getLineCharCode(2, 4), '2'.charCodeAt(0), '2'); + }); }); suite('search offset cache', () => { diff --git a/src/vs/editor/test/common/model/textModel.test.ts b/src/vs/editor/test/common/model/textModel.test.ts index 907e1460e72..e6d83dbe0ba 100644 --- a/src/vs/editor/test/common/model/textModel.test.ts +++ b/src/vs/editor/test/common/model/textModel.test.ts @@ -8,19 +8,16 @@ import * as assert from 'assert'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { TextModel, createTextBuffer } from 'vs/editor/common/model/textModel'; -import { DefaultEndOfLine } from 'vs/editor/common/model'; import { UTF8_BOM_CHARACTER } from 'vs/base/common/strings'; +import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; function testGuessIndentation(defaultInsertSpaces: boolean, defaultTabSize: number, expectedInsertSpaces: boolean, expectedTabSize: number, text: string[], msg?: string): void { - var m = TextModel.createFromString( + var m = createTextModel( text.join('\n'), { - isForSimpleWidget: false, tabSize: defaultTabSize, insertSpaces: defaultInsertSpaces, - detectIndentation: true, - defaultEOL: DefaultEndOfLine.LF, - trimAutoWhitespace: true + detectIndentation: true } ); var r = m.getOptions(); @@ -706,14 +703,9 @@ suite('Editor Model - TextModel', () => { }); test('normalizeIndentation 1', () => { - let model = TextModel.createFromString('', + let model = createTextModel('', { - isForSimpleWidget: false, - detectIndentation: false, - tabSize: 4, - insertSpaces: false, - trimAutoWhitespace: true, - defaultEOL: DefaultEndOfLine.LF + insertSpaces: false } ); @@ -743,16 +735,7 @@ suite('Editor Model - TextModel', () => { }); test('normalizeIndentation 2', () => { - let model = TextModel.createFromString('', - { - isForSimpleWidget: false, - detectIndentation: false, - tabSize: 4, - insertSpaces: true, - trimAutoWhitespace: true, - defaultEOL: DefaultEndOfLine.LF - } - ); + let model = createTextModel(''); assert.equal(model.normalizeIndentation('\ta'), ' a'); assert.equal(model.normalizeIndentation(' a'), ' a'); diff --git a/src/vs/editor/test/common/model/textModelWithTokens.test.ts b/src/vs/editor/test/common/model/textModelWithTokens.test.ts index a9fae3df41a..be4d9452721 100644 --- a/src/vs/editor/test/common/model/textModelWithTokens.test.ts +++ b/src/vs/editor/test/common/model/textModelWithTokens.test.ts @@ -380,10 +380,39 @@ suite('TextModel.getLineIndentGuide', () => { actual[line - 1] = [actualIndents[line - 1], model.getLineContent(line)]; } - // let expected = lines.map(l => l[0]); - assert.deepEqual(actual, lines); + // Also test getActiveIndentGuide + for (let lineNumber = 1; lineNumber <= model.getLineCount(); lineNumber++) { + let startLineNumber = lineNumber; + let endLineNumber = lineNumber; + let indent = actualIndents[lineNumber - 1]; + + if (indent !== 0) { + for (let i = lineNumber - 1; i >= 1; i--) { + const currIndent = actualIndents[i - 1]; + if (currIndent >= indent) { + startLineNumber = i; + } else { + break; + } + } + for (let i = lineNumber + 1; i <= model.getLineCount(); i++) { + const currIndent = actualIndents[i - 1]; + if (currIndent >= indent) { + endLineNumber = i; + } else { + break; + } + } + } + + const expected = { startLineNumber, endLineNumber, indent }; + const actual = model.getActiveIndentGuide(lineNumber); + + assert.deepEqual(actual, expected, `line number ${lineNumber}`); + } + model.dispose(); } diff --git a/src/vs/editor/test/common/services/editorSimpleWorker.test.ts b/src/vs/editor/test/common/services/editorSimpleWorker.test.ts index 0a8b62e8a55..0d55c58d7c1 100644 --- a/src/vs/editor/test/common/services/editorSimpleWorker.test.ts +++ b/src/vs/editor/test/common/services/editorSimpleWorker.test.ts @@ -168,4 +168,25 @@ suite('EditorSimpleWorker', () => { assert.equal(suggestions[0].label, 'foobar'); }); }); + + test('get words via iterator, issue #46930', function () { + + let model = worker.addModel([ + 'one line', // 1 + 'two line', // 2 + '', + 'past empty', + 'single', + '', + 'and now we are done' + ]); + + let words: string[] = []; + + for (let iter = model.createWordIterator(/[a-z]+/img), e = iter.next(); !e.done; e = iter.next()) { + words.push(e.value); + } + + assert.deepEqual(words, ['one', 'line', 'two', 'line', 'past', 'empty', 'single', 'and', 'now', 'we', 'are', 'done']); + }); }); diff --git a/src/vs/editor/test/common/services/modelService.test.ts b/src/vs/editor/test/common/services/modelService.test.ts index d6f7ab26ce4..fa75cbb9c4f 100644 --- a/src/vs/editor/test/common/services/modelService.test.ts +++ b/src/vs/editor/test/common/services/modelService.test.ts @@ -93,7 +93,7 @@ suite('ModelService', () => { const actual = ModelServiceImpl._computeEdits(model, textBuffer); assert.deepEqual(actual, [ - EditOperation.replace(new Range(1, 1, 2, 1), 'This is line One\n') + EditOperation.replaceMove(new Range(1, 1, 2, 1), 'This is line One\n') ]); }); @@ -147,7 +147,7 @@ suite('ModelService', () => { const actual = ModelServiceImpl._computeEdits(model, textBuffer); assert.deepEqual(actual, [ - EditOperation.replace( + EditOperation.replaceMove( new Range(1, 1, 4, 1), [ 'This is line One', @@ -182,7 +182,7 @@ suite('ModelService', () => { const actual = ModelServiceImpl._computeEdits(model, textBuffer); assert.deepEqual(actual, [ - EditOperation.replace(new Range(3, 2, 3, 2), '\r\n') + EditOperation.replaceMove(new Range(3, 2, 3, 2), '\r\n') ]); }); diff --git a/src/vs/editor/test/common/viewLayout/editorLayoutProvider.test.ts b/src/vs/editor/test/common/viewLayout/editorLayoutProvider.test.ts index eddae34ccc2..8009ce8a0dd 100644 --- a/src/vs/editor/test/common/viewLayout/editorLayoutProvider.test.ts +++ b/src/vs/editor/test/common/viewLayout/editorLayoutProvider.test.ts @@ -58,7 +58,7 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { renderMinimap: RenderMinimap.None, minimapLeft: 0, minimapWidth: 0, - viewportColumn: 99, + viewportColumn: 98, verticalScrollbarWidth: 0, horizontalScrollbarHeight: 0, @@ -174,7 +174,7 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { renderMinimap: RenderMinimap.None, minimapLeft: 0, minimapWidth: 0, - viewportColumn: 89, + viewportColumn: 88, verticalScrollbarWidth: 0, horizontalScrollbarHeight: 0, @@ -232,7 +232,7 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { renderMinimap: RenderMinimap.None, minimapLeft: 0, minimapWidth: 0, - viewportColumn: 89, + viewportColumn: 88, verticalScrollbarWidth: 0, horizontalScrollbarHeight: 0, @@ -290,7 +290,7 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { renderMinimap: RenderMinimap.None, minimapLeft: 0, minimapWidth: 0, - viewportColumn: 89, + viewportColumn: 88, verticalScrollbarWidth: 0, horizontalScrollbarHeight: 0, @@ -348,7 +348,7 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { renderMinimap: RenderMinimap.None, minimapLeft: 0, minimapWidth: 0, - viewportColumn: 84, + viewportColumn: 83, verticalScrollbarWidth: 0, horizontalScrollbarHeight: 0, @@ -406,7 +406,7 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { renderMinimap: RenderMinimap.None, minimapLeft: 0, minimapWidth: 0, - viewportColumn: 84, + viewportColumn: 83, verticalScrollbarWidth: 0, horizontalScrollbarHeight: 0, @@ -464,7 +464,7 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { renderMinimap: RenderMinimap.None, minimapLeft: 0, minimapWidth: 0, - viewportColumn: 83, + viewportColumn: 82, verticalScrollbarWidth: 0, horizontalScrollbarHeight: 0, @@ -522,7 +522,7 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { renderMinimap: RenderMinimap.None, minimapLeft: 0, minimapWidth: 0, - viewportColumn: 172, + viewportColumn: 171, verticalScrollbarWidth: 0, horizontalScrollbarHeight: 0, @@ -580,7 +580,7 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { renderMinimap: RenderMinimap.None, minimapLeft: 0, minimapWidth: 0, - viewportColumn: 170, + viewportColumn: 169, verticalScrollbarWidth: 0, horizontalScrollbarHeight: 0, @@ -632,13 +632,13 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { decorationsHeight: 800, contentLeft: 10, - contentWidth: 900, + contentWidth: 901, contentHeight: 800, renderMinimap: RenderMinimap.Small, - minimapLeft: 910, - minimapWidth: 90, - viewportColumn: 90, + minimapLeft: 911, + minimapWidth: 89, + viewportColumn: 89, verticalScrollbarWidth: 0, horizontalScrollbarHeight: 0, @@ -690,13 +690,13 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { decorationsHeight: 800, contentLeft: 10, - contentWidth: 900, + contentWidth: 901, contentHeight: 800, renderMinimap: RenderMinimap.Large, - minimapLeft: 910, - minimapWidth: 90, - viewportColumn: 90, + minimapLeft: 911, + minimapWidth: 89, + viewportColumn: 89, verticalScrollbarWidth: 0, horizontalScrollbarHeight: 0, @@ -825,4 +825,63 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { } }); }); + + test('issue #31312: When wrapping, leave 2px for the cursor', () => { + doTest({ + outerWidth: 1201, + outerHeight: 422, + showGlyphMargin: true, + lineHeight: 30, + showLineNumbers: true, + lineNumbersMinChars: 3, + lineNumbersDigitCount: 1, + lineDecorationsWidth: 26, + typicalHalfwidthCharacterWidth: 12.04296875, + maxDigitWidth: 12.04296875, + verticalScrollbarWidth: 14, + horizontalScrollbarHeight: 10, + scrollbarArrowSize: 11, + verticalScrollbarHasArrows: false, + minimap: true, + minimapSide: 'right', + minimapRenderCharacters: true, + minimapMaxColumn: 120, + pixelRatio: 2 + }, { + width: 1201, + height: 422, + + glyphMarginLeft: 0, + glyphMarginWidth: 30, + glyphMarginHeight: 422, + + lineNumbersLeft: 30, + lineNumbersWidth: 36, + lineNumbersHeight: 422, + + decorationsLeft: 66, + decorationsWidth: 26, + decorationsHeight: 422, + + contentLeft: 92, + contentWidth: 1026, + contentHeight: 422, + + renderMinimap: RenderMinimap.Large, + minimapLeft: 1104, + minimapWidth: 83, + viewportColumn: 83, + + verticalScrollbarWidth: 14, + horizontalScrollbarHeight: 10, + + overviewRuler: { + top: 0, + width: 14, + height: 422, + right: 0 + } + }); + + }); }); diff --git a/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts b/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts index b0214ecf8d9..550eb85bf20 100644 --- a/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts +++ b/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts @@ -12,6 +12,7 @@ import { MetadataConsts } from 'vs/editor/common/modes'; import { LineDecoration } from 'vs/editor/common/viewLayout/lineDecorations'; import { InlineDecorationType } from 'vs/editor/common/viewModel/viewModel'; import { IViewLineTokens } from 'vs/editor/common/core/lineTokens'; +import * as strings from 'vs/base/common/strings'; function createViewLineTokens(viewLineTokens: ViewLineToken[]): IViewLineTokens { return new ViewLineTokens(viewLineTokens); @@ -29,6 +30,7 @@ suite('viewLineRenderer.renderLine', () => { let _actual = renderViewLine(new RenderLineInput( false, lineContent, + strings.isBasicASCII(lineContent), false, 0, createViewLineTokens([new ViewLineToken(lineContent.length, 0)]), @@ -75,6 +77,7 @@ suite('viewLineRenderer.renderLine', () => { let _actual = renderViewLine(new RenderLineInput( false, lineContent, + true, false, 0, createViewLineTokens(parts), @@ -111,6 +114,7 @@ suite('viewLineRenderer.renderLine', () => { let _actual = renderViewLine(new RenderLineInput( false, 'Hello world!', + true, false, 0, createViewLineTokens([ @@ -212,6 +216,7 @@ suite('viewLineRenderer.renderLine', () => { let _actual = renderViewLine(new RenderLineInput( false, lineText, + true, false, 0, lineParts, @@ -271,6 +276,7 @@ suite('viewLineRenderer.renderLine', () => { let _actual = renderViewLine(new RenderLineInput( false, lineText, + true, false, 0, lineParts, @@ -330,6 +336,7 @@ suite('viewLineRenderer.renderLine', () => { let _actual = renderViewLine(new RenderLineInput( false, lineText, + true, false, 0, lineParts, @@ -366,6 +373,7 @@ suite('viewLineRenderer.renderLine', () => { let _actual = renderViewLine(new RenderLineInput( false, lineText, + false, true, 0, lineParts, @@ -393,6 +401,7 @@ suite('viewLineRenderer.renderLine', () => { let actual = renderViewLine(new RenderLineInput( false, lineText, + true, false, 0, lineParts, @@ -490,6 +499,7 @@ suite('viewLineRenderer.renderLine', () => { let actual = renderViewLine(new RenderLineInput( false, lineText, + true, false, 0, lineParts, @@ -524,6 +534,7 @@ suite('viewLineRenderer.renderLine', () => { false, lineText, false, + false, 0, lineParts, [], @@ -535,11 +546,7 @@ suite('viewLineRenderer.renderLine', () => { false )); let expectedOutput = [ - 'a𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷', - '𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷', - '𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷', - '𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷', - '𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷', + 'a𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷', ]; assert.equal(actual.html, '' + expectedOutput.join('') + ''); }); @@ -553,6 +560,7 @@ suite('viewLineRenderer.renderLine', () => { let actual = renderViewLine(new RenderLineInput( false, lineText, + false, true, 0, lineParts, @@ -596,6 +604,7 @@ suite('viewLineRenderer.renderLine', () => { let _actual = renderViewLine(new RenderLineInput( true, lineText, + true, false, 4, lineParts, @@ -676,6 +685,7 @@ suite('viewLineRenderer.renderLine 2', () => { let actual = renderViewLine(new RenderLineInput( fontIsMonospace, lineContent, + true, false, fauxIndentLength, createViewLineTokens(tokens), @@ -698,6 +708,7 @@ suite('viewLineRenderer.renderLine 2', () => { let actual = renderViewLine(new RenderLineInput( false, lineContent, + true, false, 0, createViewLineTokens([createPart(21, 3)]), @@ -726,6 +737,7 @@ suite('viewLineRenderer.renderLine 2', () => { let actual = renderViewLine(new RenderLineInput( true, lineContent, + true, false, 0, createViewLineTokens([ @@ -990,6 +1002,7 @@ suite('viewLineRenderer.renderLine 2', () => { let actual = renderViewLine(new RenderLineInput( false, 'Hello world', + true, false, 0, createViewLineTokens([createPart(11, 0)]), @@ -1031,6 +1044,7 @@ suite('viewLineRenderer.renderLine 2', () => { let actual = renderViewLine(new RenderLineInput( false, lineContent, + true, false, 0, createViewLineTokens([createPart(4, 3)]), @@ -1060,6 +1074,7 @@ suite('viewLineRenderer.renderLine 2', () => { let actual = renderViewLine(new RenderLineInput( false, lineContent, + true, false, 0, createViewLineTokens([createPart(4, 3)]), @@ -1090,6 +1105,7 @@ suite('viewLineRenderer.renderLine 2', () => { let actual = renderViewLine(new RenderLineInput( false, lineContent, + true, false, 0, createViewLineTokens([createPart(0, 3)]), @@ -1117,6 +1133,7 @@ suite('viewLineRenderer.renderLine 2', () => { true, ' 1. 🙏', false, + false, 0, createViewLineTokens([createPart(7, 3)]), [new LineDecoration(7, 8, 'inline-folded', InlineDecorationType.After)], @@ -1143,6 +1160,7 @@ suite('viewLineRenderer.renderLine 2', () => { let actual = renderViewLine(new RenderLineInput( true, '', + true, false, 0, createViewLineTokens([createPart(0, 3)]), @@ -1172,6 +1190,7 @@ suite('viewLineRenderer.renderLine 2', () => { let actual = renderViewLine(new RenderLineInput( true, '\t}', + true, false, 0, createViewLineTokens([createPart(2, 3)]), @@ -1203,6 +1222,7 @@ suite('viewLineRenderer.renderLine 2', () => { true, 'asd = "擦"\t\t#asd', false, + false, 0, createViewLineTokens([createPart(15, 3)]), [], @@ -1229,6 +1249,7 @@ suite('viewLineRenderer.renderLine 2', () => { true, 'asd = "擦"\t\t#asd', false, + false, 0, createViewLineTokens([createPart(15, 3)]), [], @@ -1255,10 +1276,96 @@ suite('viewLineRenderer.renderLine 2', () => { assert.deepEqual(actual.html, expected); }); + test('issue #22352: COMBINING ACUTE ACCENT (U+0301)', () => { + + let actual = renderViewLine(new RenderLineInput( + true, + '12345689012345678901234568901234567890123456890abába', + false, + false, + 0, + createViewLineTokens([createPart(53, 3)]), + [], + 4, + 10, + 10000, + 'none', + false, + false + )); + + let expected = [ + '', + '12345689012345678901234568901234567890123456890abába', + '' + ].join(''); + + assert.deepEqual(actual.html, expected); + }); + + test('issue #22352: Partially Broken Complex Script Rendering of Tamil', () => { + + let actual = renderViewLine(new RenderLineInput( + true, + ' JoyShareல் பின்தொடர்ந்து, விடீயோ, ஜோக்குகள், அனிமேசன், நகைச்சுவை படங்கள் மற்றும் செய்திகளை பெறுவீர்', + false, + false, + 0, + createViewLineTokens([createPart(100, 3)]), + [], + 4, + 10, + 10000, + 'none', + false, + false + )); + + let expected = [ + '', + '\u00a0JoyShareல்\u00a0பின்தொடர்ந்து,\u00a0விடீயோ,\u00a0ஜோக்குகள்,\u00a0அனிமேசன்,\u00a0நகைச்சுவை\u00a0படங்கள்\u00a0மற்றும்\u00a0செய்திகளை\u00a0பெறுவீர்', + '' + ].join(''); + + let _expected = expected.split('').map(c => c.charCodeAt(0)); + let _actual = actual.html.split('').map(c => c.charCodeAt(0)); + assert.deepEqual(_actual, _expected); + + assert.deepEqual(actual.html, expected); + }); + + test('issue #42700: Hindi characters are not being rendered properly', () => { + + let actual = renderViewLine(new RenderLineInput( + true, + ' वो ऐसा क्या है जो हमारे अंदर भी है और बाहर भी है। जिसकी वजह से हम सब हैं। जिसने इस सृष्टि की रचना की है।', + false, + false, + 0, + createViewLineTokens([createPart(105, 3)]), + [], + 4, + 10, + 10000, + 'none', + false, + false + )); + + let expected = [ + '', + '\u00a0वो\u00a0ऐसा\u00a0क्या\u00a0है\u00a0जो\u00a0हमारे\u00a0अंदर\u00a0भी\u00a0है\u00a0और\u00a0बाहर\u00a0भी\u00a0है।\u00a0जिसकी\u00a0वजह\u00a0से\u00a0हम\u00a0सब\u00a0हैं।\u00a0जिसने\u00a0इस\u00a0सृष्टि\u00a0की\u00a0रचना\u00a0की\u00a0है।', + '' + ].join(''); + + assert.deepEqual(actual.html, expected); + }); + function createTestGetColumnOfLinePartOffset(lineContent: string, tabSize: number, parts: ViewLineToken[], expectedPartLengths: number[]): (partIndex: number, partLength: number, offset: number, expected: number) => void { let renderLineOutput = renderViewLine(new RenderLineInput( false, lineContent, + true, false, 0, createViewLineTokens(parts), diff --git a/src/vs/editor/test/common/viewModel/splitLinesCollection.test.ts b/src/vs/editor/test/common/viewModel/splitLinesCollection.test.ts index e2c676defac..1ac0489684a 100644 --- a/src/vs/editor/test/common/viewModel/splitLinesCollection.test.ts +++ b/src/vs/editor/test/common/viewModel/splitLinesCollection.test.ts @@ -780,6 +780,9 @@ function createModel(text: string): ISimpleModel { getLineContent: (lineNumber: number) => { return text; }, + getLineLength: (lineNumber: number) => { + return text.length; + }, getLineMinColumn: (lineNumber: number) => { return 1; }, diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index e153cfedbaa..268571ebc7d 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -1193,6 +1193,11 @@ declare namespace monaco.editor { * Should the decoration expand to encompass a whole line. */ isWholeLine?: boolean; + /** + * Specifies the stack order of a decoration. + * A decoration with greater stack order is always in front of a decoration with a lower stack order. + */ + zIndex?: number; /** * If set, render this decoration in the overview ruler. */ @@ -1215,6 +1220,10 @@ declare namespace monaco.editor { * to have a background color decoration. */ inlineClassName?: string; + /** + * If there is an `inlineClassName` which affects letter spacing. + */ + inlineClassNameAffectsLetterSpacing?: boolean; /** * If set, the decoration will be rendered before the text with this CSS class name. */ @@ -1483,6 +1492,10 @@ declare namespace monaco.editor { * Get the text for a certain line. */ getLineContent(lineNumber: number): string; + /** + * Get the text length for a certain line. + */ + getLineLength(lineNumber: number): number; /** * Get the text for all lines. */ @@ -1927,9 +1940,13 @@ declare namespace monaco.editor { * A (serializable) state of the view. */ export interface IViewState { - scrollTop: number; - scrollTopWithoutViewZones: number; + /** written by previous versions */ + scrollTop?: number; + /** written by previous versions */ + scrollTopWithoutViewZones?: number; scrollLeft: number; + firstPosition: IPosition; + firstPositionDeltaTop: number; } /** @@ -2187,6 +2204,10 @@ declare namespace monaco.editor { * The range that got replaced. */ readonly range: IRange; + /** + * The offset of the range that got replaced. + */ + readonly rangeOffset: number; /** * The length of the range that got replaced. */ @@ -2684,6 +2705,11 @@ declare namespace monaco.editor { * Defaults to 'alt' */ multiCursorModifier?: 'ctrlCmd' | 'alt'; + /** + * Merge overlapping selections. + * Defaults to true + */ + multiCursorMergeOverlapping?: boolean; /** * Configure the editor's accessibility support. * Defaults to 'auto'. It is best to leave this to 'auto'. @@ -2802,6 +2828,11 @@ declare namespace monaco.editor { * Defaults to true. */ folding?: boolean; + /** + * Selects the folding strategy. 'auto' uses the strategies contributed for the current document, 'indentation' uses the indentation based folding strategy. + * Defaults to 'auto'. + */ + foldingStrategy?: 'auto' | 'indentation'; /** * Controls whether the fold actions in the gutter stay always visible or hide unless the mouse is over the gutter. * Defaults to 'mouseover'. @@ -3081,6 +3112,7 @@ declare namespace monaco.editor { readonly occurrencesHighlight: boolean; readonly codeLens: boolean; readonly folding: boolean; + readonly foldingStrategy: 'auto' | 'indentation'; readonly showFoldingControls: 'always' | 'mouseover'; readonly matchBrackets: boolean; readonly find: InternalEditorFindOptions; @@ -3099,6 +3131,7 @@ declare namespace monaco.editor { readonly lineHeight: number; readonly readOnly: boolean; readonly multiCursorModifier: 'altKey' | 'ctrlKey' | 'metaKey'; + readonly multiCursorMergeOverlapping: boolean; readonly wordSeparators: string; readonly autoClosingBrackets: boolean; readonly autoIndent: boolean; @@ -3236,6 +3269,7 @@ declare namespace monaco.editor { readonly readOnly: boolean; readonly accessibilitySupport: boolean; readonly multiCursorModifier: boolean; + readonly multiCursorMergeOverlapping: boolean; readonly wordSeparators: boolean; readonly autoClosingBrackets: boolean; readonly autoIndent: boolean; @@ -3752,10 +3786,6 @@ declare namespace monaco.editor { * Get the layout info for the editor. */ getLayoutInfo(): EditorLayoutInfo; - /** - * Returns the range that is currently centered in the view port. - */ - getCenteredRangeInViewport(): Range; /** * Returns the ranges that are currently visible. * Does not account for horizontal scrolling. @@ -4075,8 +4105,10 @@ declare namespace monaco.languages { export function registerColorProvider(languageId: string, provider: DocumentColorProvider): IDisposable; /** - * Register a folding provider + * Register a folding range provider */ + export function registerFoldingRangeProvider(languageId: string, provider: FoldingRangeProvider): IDisposable; + /** * Contains additional diagnostic information about the context in which * a [code action](#CodeActionProvider.provideCodeActions) is run. @@ -4526,7 +4558,7 @@ declare namespace monaco.languages { * editor will use the range at the current position or the * current position itself. */ - range: IRange; + range?: IRange; } /** @@ -4970,6 +5002,60 @@ declare namespace monaco.languages { provideColorPresentations(model: editor.ITextModel, colorInfo: IColorInformation, token: CancellationToken): IColorPresentation[] | Thenable; } + export interface FoldingContext { + } + + /** + * A provider of colors for editor models. + */ + export interface FoldingRangeProvider { + /** + * Provides the color ranges for a specific model. + */ + provideFoldingRanges(model: editor.ITextModel, context: FoldingContext, token: CancellationToken): FoldingRange[] | Thenable; + } + + export interface FoldingRange { + /** + * The zero-based start line of the range to fold. The folded area starts after the line's last character. + */ + start: number; + /** + * The zero-based end line of the range to fold. The folded area ends with the line's last character. + */ + end: number; + /** + * Describes the [Kind](#FoldingRangeKind) of the folding range such as [Comment](#FoldingRangeKind.Comment) or + * [Region](#FoldingRangeKind.Region). The kind is used to categorize folding ranges and used by commands + * like 'Fold all comments'. See + * [FoldingRangeKind](#FoldingRangeKind) for an enumeration of standardized kinds. + */ + kind?: FoldingRangeKind; + } + + export class FoldingRangeKind { + value: string; + /** + * Kind for folding range representing a comment. The value of the kind is 'comment'. + */ + static readonly Comment: FoldingRangeKind; + /** + * Kind for folding range representing a import. The value of the kind is 'imports'. + */ + static readonly Imports: FoldingRangeKind; + /** + * Kind for folding range representing regions (for example marked by `#region`, `#endregion`). + * The value of the kind is 'region'. + */ + static readonly Region: FoldingRangeKind; + /** + * Creates a new [FoldingRangeKind](#FoldingRangeKind). + * + * @param value of the kind. + */ + constructor(value: string); + } + export interface ResourceFileEdit { oldUri: Uri; newUri: Uri; @@ -4986,15 +5072,14 @@ declare namespace monaco.languages { rejectReason?: string; } - export interface RenameContext { + export interface RenameLocation { range: IRange; text: string; - message?: string; } export interface RenameProvider { provideRenameEdits(model: editor.ITextModel, position: Position, newName: string, token: CancellationToken): WorkspaceEdit | Thenable; - resolveRenameLocation?(model: editor.ITextModel, position: Position, token: CancellationToken): RenameContext | Thenable; + resolveRenameLocation?(model: editor.ITextModel, position: Position, token: CancellationToken): RenameLocation | Thenable; } export interface Command { diff --git a/src/vs/platform/actions/browser/menuItemActionItem.ts b/src/vs/platform/actions/browser/menuItemActionItem.ts index 0c02288e91a..b93c8334898 100644 --- a/src/vs/platform/actions/browser/menuItemActionItem.ts +++ b/src/vs/platform/actions/browser/menuItemActionItem.ts @@ -14,7 +14,6 @@ import { ActionItem, Separator } from 'vs/base/browser/ui/actionbar/actionbar'; import { domEvent } from 'vs/base/browser/event'; import { Emitter } from 'vs/base/common/event'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { memoize } from 'vs/base/common/decorators'; import { IdGenerator } from 'vs/base/common/idGenerator'; import { createCSSRule } from 'vs/base/browser/dom'; import URI from 'vs/base/common/uri'; @@ -26,6 +25,7 @@ class AlternativeKeyEmitter extends Emitter { private _subscriptions: IDisposable[] = []; private _isPressed: boolean; + private static instance: AlternativeKeyEmitter; private constructor(contextMenuService: IContextMenuService) { super(); @@ -49,9 +49,12 @@ class AlternativeKeyEmitter extends Emitter { this.fire(this._isPressed); } - @memoize static getInstance(contextMenuService: IContextMenuService) { - return new AlternativeKeyEmitter(contextMenuService); + if (!AlternativeKeyEmitter.instance) { + AlternativeKeyEmitter.instance = new AlternativeKeyEmitter(contextMenuService); + } + + return AlternativeKeyEmitter.instance; } dispose() { diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index d125b035683..1ea711884b8 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -59,6 +59,7 @@ export class MenuId { static readonly ViewTitle = new MenuId(); static readonly ViewItemContext = new MenuId(); static readonly TouchBarContext = new MenuId(); + static readonly SearchContext = new MenuId(); readonly id: string = String(MenuId.ID++); } diff --git a/src/vs/platform/configuration/common/configuration.ts b/src/vs/platform/configuration/common/configuration.ts index 6671682407e..6aa47675ddb 100644 --- a/src/vs/platform/configuration/common/configuration.ts +++ b/src/vs/platform/configuration/common/configuration.ts @@ -12,7 +12,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IConfigurationRegistry, Extensions, OVERRIDE_PROPERTY_PATTERN } from 'vs/platform/configuration/common/configurationRegistry'; -import { StrictResourceMap } from 'vs/base/common/map'; +import { ResourceMap } from 'vs/base/common/map'; export const IConfigurationService = createDecorator('configurationService'); @@ -47,7 +47,7 @@ export interface IConfigurationChangeEvent { // Following data is used for Extension host configuration event changedConfiguration: IConfigurationModel; - changedConfigurationByResource: StrictResourceMap; + changedConfigurationByResource: ResourceMap; } export interface IConfigurationService { @@ -112,6 +112,7 @@ export interface IConfigurationData { user: IConfigurationModel; workspace: IConfigurationModel; folders: { [folder: string]: IConfigurationModel }; + isComplete: boolean; } export function compare(from: IConfigurationModel, to: IConfigurationModel): { added: string[], removed: string[], updated: string[] } { diff --git a/src/vs/platform/configuration/common/configurationModels.ts b/src/vs/platform/configuration/common/configurationModels.ts index deb168d32ac..9944e02325f 100644 --- a/src/vs/platform/configuration/common/configurationModels.ts +++ b/src/vs/platform/configuration/common/configurationModels.ts @@ -5,7 +5,7 @@ 'use strict'; import * as json from 'vs/base/common/json'; -import { StrictResourceMap } from 'vs/base/common/map'; +import { ResourceMap } from 'vs/base/common/map'; import * as arrays from 'vs/base/common/arrays'; import * as types from 'vs/base/common/types'; import * as objects from 'vs/base/common/objects'; @@ -278,15 +278,15 @@ export class ConfigurationModelParser { export class Configuration { private _workspaceConsolidatedConfiguration: ConfigurationModel = null; - private _foldersConsolidatedConfigurations: StrictResourceMap = new StrictResourceMap(); + private _foldersConsolidatedConfigurations: ResourceMap = new ResourceMap(); constructor( private _defaultConfiguration: ConfigurationModel, private _userConfiguration: ConfigurationModel, private _workspaceConfiguration: ConfigurationModel = new ConfigurationModel(), - private _folderConfigurations: StrictResourceMap = new StrictResourceMap(), + private _folderConfigurations: ResourceMap = new ResourceMap(), private _memoryConfiguration: ConfigurationModel = new ConfigurationModel(), - private _memoryConfigurationByResource: StrictResourceMap = new StrictResourceMap(), + private _memoryConfigurationByResource: ResourceMap = new ResourceMap(), private _freeze: boolean = true) { } @@ -394,7 +394,7 @@ export class Configuration { return this._workspaceConfiguration; } - protected get folders(): StrictResourceMap { + protected get folders(): ResourceMap { return this._folderConfigurations; } @@ -479,7 +479,8 @@ export class Configuration { const { contents, overrides, keys } = this._folderConfigurations.get(folder); result[folder.toString()] = { contents, overrides, keys }; return result; - }, Object.create({})) + }, Object.create({})), + isComplete: true }; } @@ -533,7 +534,7 @@ export class ConfigurationChangeEvent extends AbstractConfigurationChangeEvent i constructor( private _changedConfiguration: ConfigurationModel = new ConfigurationModel(), - private _changedConfigurationByResource: StrictResourceMap = new StrictResourceMap()) { + private _changedConfigurationByResource: ResourceMap = new ResourceMap()) { super(); } @@ -541,7 +542,7 @@ export class ConfigurationChangeEvent extends AbstractConfigurationChangeEvent i return this._changedConfiguration; } - get changedConfigurationByResource(): StrictResourceMap { + get changedConfigurationByResource(): ResourceMap { return this._changedConfigurationByResource; } diff --git a/src/vs/platform/configuration/common/configurationRegistry.ts b/src/vs/platform/configuration/common/configurationRegistry.ts index 1db8bcc8ff4..30d8205f48d 100644 --- a/src/vs/platform/configuration/common/configurationRegistry.ts +++ b/src/vs/platform/configuration/common/configurationRegistry.ts @@ -11,7 +11,6 @@ import { Registry } from 'vs/platform/registry/common/platform'; import * as types from 'vs/base/common/types'; import * as strings from 'vs/base/common/strings'; import { IJSONContributionRegistry, Extensions as JSONExtensions } from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; -import { deepClone } from 'vs/base/common/objects'; export const Extensions = { Configuration: 'base.contributions.configuration' @@ -63,13 +62,13 @@ export interface IConfigurationRegistry { } export enum ConfigurationScope { - WINDOW = 1, - RESOURCE + APPLICATION = 1, + WINDOW, + RESOURCE, } export interface IConfigurationPropertySchema extends IJSONSchema { overridable?: boolean; - isExecutable?: boolean; scope?: ConfigurationScope; notMultiRootAdopted?: boolean; included?: boolean; @@ -93,8 +92,10 @@ export interface IDefaultConfigurationExtension { defaults: { [key: string]: {} }; } -export const settingsSchema: IJSONSchema = { properties: {}, patternProperties: {}, additionalProperties: false, errorMessage: 'Unknown configuration setting' }; -export const resourceSettingsSchema: IJSONSchema = { properties: {}, patternProperties: {}, additionalProperties: false, errorMessage: 'Unknown configuration setting' }; +export const allSettings: { properties: {}, patternProperties: {} } = { properties: {}, patternProperties: {} }; +export const applicationSettings: { properties: {}, patternProperties: {} } = { properties: {}, patternProperties: {} }; +export const windowSettings: { properties: {}, patternProperties: {} } = { properties: {}, patternProperties: {} }; +export const resourceSettings: { properties: {}, patternProperties: {} } = { properties: {}, patternProperties: {} }; export const editorConfigurationSchemaId = 'vscode://schemas/settings/editor'; const contributionRegistry = Registry.as(JSONExtensions.JSONContribution); @@ -239,10 +240,17 @@ class ConfigurationRegistry implements IConfigurationRegistry { let properties = configuration.properties; if (properties) { for (let key in properties) { - settingsSchema.properties[key] = properties[key]; - resourceSettingsSchema.properties[key] = deepClone(properties[key]); - if (properties[key].scope !== ConfigurationScope.RESOURCE) { - resourceSettingsSchema.properties[key].doNotSuggest = true; + allSettings.properties[key] = properties[key]; + switch (properties[key].scope) { + case ConfigurationScope.APPLICATION: + applicationSettings.properties[key] = properties[key]; + break; + case ConfigurationScope.WINDOW: + windowSettings.properties[key] = properties[key]; + break; + case ConfigurationScope.RESOURCE: + resourceSettings.properties[key] = properties[key]; + break; } } } @@ -262,7 +270,7 @@ class ConfigurationRegistry implements IConfigurationRegistry { } private updateOverridePropertyPatternKey(): void { - let patternProperties: IJSONSchema = settingsSchema.patternProperties[this.overridePropertyPattern]; + let patternProperties: IJSONSchema = allSettings.patternProperties[this.overridePropertyPattern]; if (!patternProperties) { patternProperties = { type: 'object', @@ -271,11 +279,18 @@ class ConfigurationRegistry implements IConfigurationRegistry { $ref: editorConfigurationSchemaId }; } - delete settingsSchema.patternProperties[this.overridePropertyPattern]; + + delete allSettings.patternProperties[this.overridePropertyPattern]; + delete applicationSettings.patternProperties[this.overridePropertyPattern]; + delete windowSettings.patternProperties[this.overridePropertyPattern]; + delete resourceSettings.patternProperties[this.overridePropertyPattern]; + this.computeOverridePropertyPattern(); - settingsSchema.patternProperties[this.overridePropertyPattern] = patternProperties; - resourceSettingsSchema.patternProperties[this.overridePropertyPattern] = patternProperties; + allSettings.patternProperties[this.overridePropertyPattern] = patternProperties; + applicationSettings.patternProperties[this.overridePropertyPattern] = patternProperties; + windowSettings.patternProperties[this.overridePropertyPattern] = patternProperties; + resourceSettings.patternProperties[this.overridePropertyPattern] = patternProperties; } private update(configuration: IConfigurationNode): void { diff --git a/src/vs/platform/contextkey/browser/contextKeyService.ts b/src/vs/platform/contextkey/browser/contextKeyService.ts index dd73e1c1a28..4d272d3e30e 100644 --- a/src/vs/platform/contextkey/browser/contextKeyService.ts +++ b/src/vs/platform/contextkey/browser/contextKeyService.ts @@ -7,7 +7,7 @@ import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { KeybindingResolver } from 'vs/platform/keybinding/common/keybindingResolver'; -import { IContextKey, IContext, IContextKeyServiceTarget, IContextKeyService, SET_CONTEXT_COMMAND_ID, ContextKeyExpr, IContextKeyChangeEvent } from 'vs/platform/contextkey/common/contextkey'; +import { IContextKey, IContext, IContextKeyServiceTarget, IContextKeyService, SET_CONTEXT_COMMAND_ID, ContextKeyExpr, IContextKeyChangeEvent, IReadableSet } from 'vs/platform/contextkey/common/contextkey'; import { IConfigurationService, IConfigurationChangeEvent, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; import { Event, Emitter, debounceEvent } from 'vs/base/common/event'; @@ -179,7 +179,7 @@ export class ContextKeyChangeEvent implements IContextKeyChangeEvent { this._keys = this._keys.concat(oneOrManyKeys); } - affectsSome(keys: Set): boolean { + affectsSome(keys: IReadableSet): boolean { for (const key of this._keys) { if (keys.has(key)) { return true; diff --git a/src/vs/platform/contextkey/common/contextkey.ts b/src/vs/platform/contextkey/common/contextkey.ts index 8bda07939ae..46891fc2898 100644 --- a/src/vs/platform/contextkey/common/contextkey.ts +++ b/src/vs/platform/contextkey/common/contextkey.ts @@ -554,8 +554,12 @@ export interface IContextKeyServiceTarget { export const IContextKeyService = createDecorator('contextKeyService'); +export interface IReadableSet { + has(value: T): boolean; +} + export interface IContextKeyChangeEvent { - affectsSome(keys: Set): boolean; + affectsSome(keys: IReadableSet): boolean; } export interface IContextKeyService { diff --git a/src/vs/platform/driver/common/driver.ts b/src/vs/platform/driver/common/driver.ts new file mode 100644 index 00000000000..b40326a162b --- /dev/null +++ b/src/vs/platform/driver/common/driver.ts @@ -0,0 +1,287 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { IChannel } from 'vs/base/parts/ipc/common/ipc'; + +export const ID = 'driverService'; +export const IDriver = createDecorator(ID); + +// !! Do not remove the following START and END markers, they are parsed by the smoketest build + +//*START +export interface IElement { + tagName: string; + className: string; + textContent: string; + attributes: { [name: string]: string; }; + children: IElement[]; +} + +export interface IDriver { + _serviceBrand: any; + + getWindowIds(): TPromise; + capturePage(windowId: number): TPromise; + reloadWindow(windowId: number): TPromise; + dispatchKeybinding(windowId: number, keybinding: string): TPromise; + click(windowId: number, selector: string, xoffset?: number | undefined, yoffset?: number | undefined): TPromise; + doubleClick(windowId: number, selector: string): TPromise; + move(windowId: number, selector: string): TPromise; + setValue(windowId: number, selector: string, text: string): TPromise; + paste(windowId: number, selector: string, text: string): TPromise; + getTitle(windowId: number): TPromise; + isActiveElement(windowId: number, selector: string): TPromise; + getElements(windowId: number, selector: string, recursive?: boolean): TPromise; + typeInEditor(windowId: number, selector: string, text: string): TPromise; + getTerminalBuffer(windowId: number, selector: string): TPromise; +} +//*END + +export interface IDriverChannel extends IChannel { + call(command: 'getWindowIds'): TPromise; + call(command: 'capturePage'): TPromise; + call(command: 'reloadWindow', arg: number): TPromise; + call(command: 'dispatchKeybinding', arg: [number, string]): TPromise; + call(command: 'click', arg: [number, string, number | undefined, number | undefined]): TPromise; + call(command: 'doubleClick', arg: [number, string]): TPromise; + call(command: 'move', arg: [number, string]): TPromise; + call(command: 'setValue', arg: [number, string, string]): TPromise; + call(command: 'paste', arg: [number, string, string]): TPromise; + call(command: 'getTitle', arg: [number]): TPromise; + call(command: 'isActiveElement', arg: [number, string]): TPromise; + call(command: 'getElements', arg: [number, string, boolean]): TPromise; + call(command: 'typeInEditor', arg: [number, string, string]): TPromise; + call(command: 'getTerminalBuffer', arg: [number, string]): TPromise; + call(command: string, arg: any): TPromise; +} + +export class DriverChannel implements IDriverChannel { + + constructor(private driver: IDriver) { } + + call(command: string, arg?: any): TPromise { + switch (command) { + case 'getWindowIds': return this.driver.getWindowIds(); + case 'capturePage': return this.driver.capturePage(arg); + case 'reloadWindow': return this.driver.reloadWindow(arg); + case 'dispatchKeybinding': return this.driver.dispatchKeybinding(arg[0], arg[1]); + case 'click': return this.driver.click(arg[0], arg[1], arg[2], arg[3]); + case 'doubleClick': return this.driver.doubleClick(arg[0], arg[1]); + case 'move': return this.driver.move(arg[0], arg[1]); + case 'setValue': return this.driver.setValue(arg[0], arg[1], arg[2]); + case 'paste': return this.driver.paste(arg[0], arg[1], arg[2]); + case 'getTitle': return this.driver.getTitle(arg[0]); + case 'isActiveElement': return this.driver.isActiveElement(arg[0], arg[1]); + case 'getElements': return this.driver.getElements(arg[0], arg[1], arg[2]); + case 'typeInEditor': return this.driver.typeInEditor(arg[0], arg[1], arg[2]); + case 'getTerminalBuffer': return this.driver.getTerminalBuffer(arg[0], arg[1]); + } + + return undefined; + } +} + +export class DriverChannelClient implements IDriver { + + _serviceBrand: any; + + constructor(private channel: IDriverChannel) { } + + getWindowIds(): TPromise { + return this.channel.call('getWindowIds'); + } + + capturePage(windowId: number): TPromise { + return this.channel.call('capturePage', windowId); + } + + reloadWindow(windowId: number): TPromise { + return this.channel.call('reloadWindow', windowId); + } + + dispatchKeybinding(windowId: number, keybinding: string): TPromise { + return this.channel.call('dispatchKeybinding', [windowId, keybinding]); + } + + click(windowId: number, selector: string, xoffset: number | undefined, yoffset: number | undefined): TPromise { + return this.channel.call('click', [windowId, selector, xoffset, yoffset]); + } + + doubleClick(windowId: number, selector: string): TPromise { + return this.channel.call('doubleClick', [windowId, selector]); + } + + move(windowId: number, selector: string): TPromise { + return this.channel.call('move', [windowId, selector]); + } + + setValue(windowId: number, selector: string, text: string): TPromise { + return this.channel.call('setValue', [windowId, selector, text]); + } + + paste(windowId: number, selector: string, text: string): TPromise { + return this.channel.call('paste', [windowId, selector, text]); + } + + getTitle(windowId: number): TPromise { + return this.channel.call('getTitle', [windowId]); + } + + isActiveElement(windowId: number, selector: string): TPromise { + return this.channel.call('isActiveElement', [windowId, selector]); + } + + getElements(windowId: number, selector: string, recursive: boolean): TPromise { + return this.channel.call('getElements', [windowId, selector, recursive]); + } + + typeInEditor(windowId: number, selector: string, text: string): TPromise { + return this.channel.call('typeInEditor', [windowId, selector, text]); + } + + getTerminalBuffer(windowId: number, selector: string): TPromise { + return this.channel.call('getTerminalBuffer', [windowId, selector]); + } +} + +export interface IWindowDriverRegistry { + registerWindowDriver(windowId: number): TPromise; + reloadWindowDriver(windowId: number): TPromise; +} + +export interface IWindowDriverRegistryChannel extends IChannel { + call(command: 'registerWindowDriver', arg: number): TPromise; + call(command: 'reloadWindowDriver', arg: number): TPromise; + call(command: string, arg: any): TPromise; +} + +export class WindowDriverRegistryChannel implements IWindowDriverRegistryChannel { + + constructor(private registry: IWindowDriverRegistry) { } + + call(command: string, arg?: any): TPromise { + switch (command) { + case 'registerWindowDriver': return this.registry.registerWindowDriver(arg); + case 'reloadWindowDriver': return this.registry.reloadWindowDriver(arg); + } + + return undefined; + } +} + +export class WindowDriverRegistryChannelClient implements IWindowDriverRegistry { + + _serviceBrand: any; + + constructor(private channel: IWindowDriverRegistryChannel) { } + + registerWindowDriver(windowId: number): TPromise { + return this.channel.call('registerWindowDriver', windowId); + } + + reloadWindowDriver(windowId: number): TPromise { + return this.channel.call('reloadWindowDriver', windowId); + } +} + +export interface IWindowDriver { + click(selector: string, xoffset?: number | undefined, yoffset?: number | undefined): TPromise; + doubleClick(selector: string): TPromise; + move(selector: string): TPromise; + setValue(selector: string, text: string): TPromise; + paste(selector: string, text: string): TPromise; + getTitle(): TPromise; + isActiveElement(selector: string): TPromise; + getElements(selector: string, recursive: boolean): TPromise; + typeInEditor(selector: string, text: string): TPromise; + getTerminalBuffer(selector: string): TPromise; +} + +export interface IWindowDriverChannel extends IChannel { + call(command: 'click', arg: [string, number | undefined, number | undefined]): TPromise; + call(command: 'doubleClick', arg: string): TPromise; + call(command: 'move', arg: string): TPromise; + call(command: 'setValue', arg: [string, string]): TPromise; + call(command: 'paste', arg: [string, string]): TPromise; + call(command: 'getTitle'): TPromise; + call(command: 'isActiveElement', arg: string): TPromise; + call(command: 'getElements', arg: [string, boolean]): TPromise; + call(command: 'typeInEditor', arg: [string, string]): TPromise; + call(command: 'getTerminalBuffer', arg: string): TPromise; + call(command: string, arg: any): TPromise; +} + +export class WindowDriverChannel implements IWindowDriverChannel { + + constructor(private driver: IWindowDriver) { } + + call(command: string, arg?: any): TPromise { + switch (command) { + case 'click': return this.driver.click(arg[0], arg[1], arg[2]); + case 'doubleClick': return this.driver.doubleClick(arg); + case 'move': return this.driver.move(arg); + case 'setValue': return this.driver.setValue(arg[0], arg[1]); + case 'paste': return this.driver.paste(arg[0], arg[1]); + case 'getTitle': return this.driver.getTitle(); + case 'isActiveElement': return this.driver.isActiveElement(arg); + case 'getElements': return this.driver.getElements(arg[0], arg[1]); + case 'typeInEditor': return this.driver.typeInEditor(arg[0], arg[1]); + case 'getTerminalBuffer': return this.driver.getTerminalBuffer(arg); + } + + return undefined; + } +} + +export class WindowDriverChannelClient implements IWindowDriver { + + _serviceBrand: any; + + constructor(private channel: IWindowDriverChannel) { } + + click(selector: string, xoffset?: number, yoffset?: number): TPromise { + return this.channel.call('click', [selector, xoffset, yoffset]); + } + + doubleClick(selector: string): TPromise { + return this.channel.call('doubleClick', selector); + } + + move(selector: string): TPromise { + return this.channel.call('move', selector); + } + + setValue(selector: string, text: string): TPromise { + return this.channel.call('setValue', [selector, text]); + } + + paste(selector: string, text: string): TPromise { + return this.channel.call('paste', [selector, text]); + } + + getTitle(): TPromise { + return this.channel.call('getTitle'); + } + + isActiveElement(selector: string): TPromise { + return this.channel.call('isActiveElement', selector); + } + + getElements(selector: string, recursive: boolean): TPromise { + return this.channel.call('getElements', [selector, recursive]); + } + + typeInEditor(selector: string, text: string): TPromise { + return this.channel.call('typeInEditor', [selector, text]); + } + + getTerminalBuffer(selector: string): TPromise { + return this.channel.call('getTerminalBuffer', selector); + } +} \ No newline at end of file diff --git a/src/vs/platform/driver/electron-browser/driver.ts b/src/vs/platform/driver/electron-browser/driver.ts new file mode 100644 index 00000000000..60bce74b113 --- /dev/null +++ b/src/vs/platform/driver/electron-browser/driver.ts @@ -0,0 +1,204 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { IDisposable, toDisposable, combinedDisposable } from 'vs/base/common/lifecycle'; +import { IWindowDriver, IElement, WindowDriverChannel, WindowDriverRegistryChannelClient } from 'vs/platform/driver/common/driver'; +import { IPCClient } from 'vs/base/parts/ipc/common/ipc'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { getTopLeftOffset, getClientArea } from 'vs/base/browser/dom'; +import * as electron from 'electron'; + +function serializeElement(element: Element, recursive: boolean): IElement { + const attributes = Object.create(null); + + for (let j = 0; j < element.attributes.length; j++) { + const attr = element.attributes.item(j); + attributes[attr.name] = attr.value; + } + + const children = []; + + if (recursive) { + for (let i = 0; i < element.children.length; i++) { + children.push(serializeElement(element.children.item(i), true)); + } + } + + return { + tagName: element.tagName, + className: element.className, + textContent: element.textContent || '', + attributes, + children + }; +} + +class WindowDriver implements IWindowDriver { + + constructor() { } + + async click(selector: string, xoffset?: number, yoffset?: number): TPromise { + return this._click(selector, 1, xoffset, yoffset); + } + + doubleClick(selector: string): TPromise { + return this._click(selector, 2); + } + + private async _getElementXY(selector: string, xoffset?: number, yoffset?: number): TPromise<{ x: number; y: number; }> { + const element = document.querySelector(selector); + + if (!element) { + throw new Error('Element not found'); + } + + const { left, top } = getTopLeftOffset(element as HTMLElement); + const { width, height } = getClientArea(element as HTMLElement); + let x: number, y: number; + + if ((typeof xoffset === 'number') || (typeof yoffset === 'number')) { + x = left + xoffset; + y = top + yoffset; + } else { + x = left + (width / 2); + y = top + (height / 2); + } + + x = Math.round(x); + y = Math.round(y); + + return { x, y }; + } + + private async _click(selector: string, clickCount: number, xoffset?: number, yoffset?: number): TPromise { + const { x, y } = await this._getElementXY(selector, xoffset, yoffset); + const webContents = electron.remote.getCurrentWebContents(); + webContents.sendInputEvent({ type: 'mouseDown', x, y, button: 'left', clickCount } as any); + webContents.sendInputEvent({ type: 'mouseUp', x, y, button: 'left', clickCount } as any); + + await TPromise.timeout(100); + } + + async move(selector: string): TPromise { + const { x, y } = await this._getElementXY(selector); + const webContents = electron.remote.getCurrentWebContents(); + webContents.sendInputEvent({ type: 'mouseMove', x, y } as any); + + await TPromise.timeout(100); + } + + async setValue(selector: string, text: string): TPromise { + const element = document.querySelector(selector); + + if (!element) { + throw new Error('Element not found'); + } + + const inputElement = element as HTMLInputElement; + inputElement.value = text; + + const event = new Event('input', { bubbles: true, cancelable: true }); + inputElement.dispatchEvent(event); + } + + async paste(selector: string, text: string): TPromise { + const element = document.querySelector(selector); + + if (!element) { + throw new Error('Element not found'); + } + + const inputElement = element as HTMLInputElement; + const clipboardData = new DataTransfer(); + clipboardData.setData('text/plain', text); + const event = new ClipboardEvent('paste', { clipboardData } as any); + + inputElement.dispatchEvent(event); + } + + async getTitle(): TPromise { + return document.title; + } + + async isActiveElement(selector: string): TPromise { + const element = document.querySelector(selector); + return element === document.activeElement; + } + + async getElements(selector: string, recursive: boolean): TPromise { + const query = document.querySelectorAll(selector); + const result: IElement[] = []; + + for (let i = 0; i < query.length; i++) { + const element = query.item(i); + result.push(serializeElement(element, recursive)); + } + + return result; + } + + async typeInEditor(selector: string, text: string): TPromise { + const element = document.querySelector(selector); + + if (!element) { + throw new Error('Editor not found: ' + selector); + } + + const textarea = element as HTMLTextAreaElement; + const start = textarea.selectionStart; + const newStart = start + text.length; + const value = textarea.value; + const newValue = value.substr(0, start) + text + value.substr(start); + + textarea.value = newValue; + textarea.setSelectionRange(newStart, newStart); + + const event = new Event('input', { 'bubbles': true, 'cancelable': true }); + textarea.dispatchEvent(event); + } + + async getTerminalBuffer(selector: string): TPromise { + const element = document.querySelector(selector); + + if (!element) { + throw new Error('Terminal not found: ' + selector); + } + + const xterm = (element as any).xterm; + + if (!xterm) { + throw new Error('Xterm not found: ' + selector); + } + + const lines: string[] = []; + + for (let i = 0; i < xterm.buffer.lines.length; i++) { + lines.push(xterm.buffer.translateBufferLineToString(i, true)); + } + + return lines; + } +} + +export async function registerWindowDriver( + client: IPCClient, + windowId: number, + instantiationService: IInstantiationService +): TPromise { + const windowDriver = instantiationService.createInstance(WindowDriver); + const windowDriverChannel = new WindowDriverChannel(windowDriver); + client.registerChannel('windowDriver', windowDriverChannel); + + const windowDriverRegistryChannel = client.getChannel('windowDriverRegistry'); + const windowDriverRegistry = new WindowDriverRegistryChannelClient(windowDriverRegistryChannel); + + await windowDriverRegistry.registerWindowDriver(windowId); + + const disposable = toDisposable(() => windowDriverRegistry.reloadWindowDriver(windowId)); + return combinedDisposable([disposable, client]); +} \ No newline at end of file diff --git a/src/vs/platform/driver/electron-main/driver.ts b/src/vs/platform/driver/electron-main/driver.ts new file mode 100644 index 00000000000..badeeab4a1f --- /dev/null +++ b/src/vs/platform/driver/electron-main/driver.ts @@ -0,0 +1,218 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { IDriver, DriverChannel, IElement, IWindowDriverChannel, WindowDriverChannelClient, IWindowDriverRegistry, WindowDriverRegistryChannel, IWindowDriver } from 'vs/platform/driver/common/driver'; +import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows'; +import { serve as serveNet } from 'vs/base/parts/ipc/node/ipc.net'; +import { combinedDisposable, IDisposable } from 'vs/base/common/lifecycle'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IPCServer, IClientRouter } from 'vs/base/parts/ipc/common/ipc'; +import { SimpleKeybinding, KeyCode } from 'vs/base/common/keyCodes'; +import { USLayoutResolvedKeybinding } from 'vs/platform/keybinding/common/usLayoutResolvedKeybinding'; +import { OS } from 'vs/base/common/platform'; +import { Emitter, toPromise } from 'vs/base/common/event'; + +// TODO@joao: bad layering! +import { KeybindingIO } from 'vs/workbench/services/keybinding/common/keybindingIO'; +import { ScanCodeBinding } from 'vs/workbench/services/keybinding/common/scanCode'; +import { NativeImage } from 'electron'; + +class WindowRouter implements IClientRouter { + + constructor(private windowId: number) { } + + route(command: string, arg: any): string { + return `window:${this.windowId}`; + } +} + +function isSilentKeyCode(keyCode: KeyCode) { + return keyCode < KeyCode.KEY_0; +} + +export class Driver implements IDriver, IWindowDriverRegistry { + + _serviceBrand: any; + + private registeredWindowIds = new Set(); + private reloadingWindowIds = new Set(); + private onDidReloadingChange = new Emitter(); + + constructor( + private windowServer: IPCServer, + @IWindowsMainService private windowsService: IWindowsMainService + ) { } + + async registerWindowDriver(windowId: number): TPromise { + this.registeredWindowIds.add(windowId); + this.reloadingWindowIds.delete(windowId); + this.onDidReloadingChange.fire(); + } + + async reloadWindowDriver(windowId: number): TPromise { + this.reloadingWindowIds.add(windowId); + } + + async getWindowIds(): TPromise { + return this.windowsService.getWindows() + .map(w => w.id) + .filter(id => this.registeredWindowIds.has(id) && !this.reloadingWindowIds.has(id)); + } + + async capturePage(windowId: number): TPromise { + await this.whenUnfrozen(windowId); + + const window = this.windowsService.getWindowById(windowId); + const webContents = window.win.webContents; + const image = await new Promise(c => webContents.capturePage(c)); + const buffer = image.toPNG(); + + return buffer.toString('base64'); + } + + async reloadWindow(windowId: number): TPromise { + await this.whenUnfrozen(windowId); + + const window = this.windowsService.getWindowById(windowId); + this.reloadingWindowIds.add(windowId); + this.windowsService.reload(window); + } + + async dispatchKeybinding(windowId: number, keybinding: string): TPromise { + await this.whenUnfrozen(windowId); + + const [first, second] = KeybindingIO._readUserBinding(keybinding); + + await this._dispatchKeybinding(windowId, first); + + if (second) { + await this._dispatchKeybinding(windowId, second); + } + } + + private async _dispatchKeybinding(windowId: number, keybinding: SimpleKeybinding | ScanCodeBinding): TPromise { + if (keybinding instanceof ScanCodeBinding) { + throw new Error('ScanCodeBindings not supported'); + } + + const window = this.windowsService.getWindowById(windowId); + const webContents = window.win.webContents; + const noModifiedKeybinding = new SimpleKeybinding(false, false, false, false, keybinding.keyCode); + const resolvedKeybinding = new USLayoutResolvedKeybinding(noModifiedKeybinding, OS); + const keyCode = resolvedKeybinding.getElectronAccelerator(); + + const modifiers = []; + + if (keybinding.ctrlKey) { + modifiers.push('ctrl'); + } + + if (keybinding.metaKey) { + modifiers.push('meta'); + } + + if (keybinding.shiftKey) { + modifiers.push('shift'); + } + + if (keybinding.altKey) { + modifiers.push('alt'); + } + + webContents.sendInputEvent({ type: 'keyDown', keyCode, modifiers } as any); + + if (!isSilentKeyCode(keybinding.keyCode)) { + webContents.sendInputEvent({ type: 'char', keyCode, modifiers } as any); + } + + webContents.sendInputEvent({ type: 'keyUp', keyCode, modifiers } as any); + + await TPromise.timeout(100); + } + + async click(windowId: number, selector: string, xoffset?: number, yoffset?: number): TPromise { + const windowDriver = await this.getWindowDriver(windowId); + return windowDriver.click(selector, xoffset, yoffset); + } + + async doubleClick(windowId: number, selector: string): TPromise { + const windowDriver = await this.getWindowDriver(windowId); + return windowDriver.doubleClick(selector); + } + + async move(windowId: number, selector: string): TPromise { + const windowDriver = await this.getWindowDriver(windowId); + return windowDriver.move(selector); + } + + async setValue(windowId: number, selector: string, text: string): TPromise { + const windowDriver = await this.getWindowDriver(windowId); + return windowDriver.setValue(selector, text); + } + + async paste(windowId: number, selector: string, text: string): TPromise { + const windowDriver = await this.getWindowDriver(windowId); + return windowDriver.paste(selector, text); + } + + async getTitle(windowId: number): TPromise { + const windowDriver = await this.getWindowDriver(windowId); + return windowDriver.getTitle(); + } + + async isActiveElement(windowId: number, selector: string): TPromise { + const windowDriver = await this.getWindowDriver(windowId); + return windowDriver.isActiveElement(selector); + } + + async getElements(windowId: number, selector: string, recursive: boolean): TPromise { + const windowDriver = await this.getWindowDriver(windowId); + return windowDriver.getElements(selector, recursive); + } + + async typeInEditor(windowId: number, selector: string, text: string): TPromise { + const windowDriver = await this.getWindowDriver(windowId); + return windowDriver.typeInEditor(selector, text); + } + + async getTerminalBuffer(windowId: number, selector: string): TPromise { + const windowDriver = await this.getWindowDriver(windowId); + return windowDriver.getTerminalBuffer(selector); + } + + private async getWindowDriver(windowId: number): TPromise { + await this.whenUnfrozen(windowId); + + const router = new WindowRouter(windowId); + const windowDriverChannel = this.windowServer.getChannel('windowDriver', router); + return new WindowDriverChannelClient(windowDriverChannel); + } + + private async whenUnfrozen(windowId: number): TPromise { + while (this.reloadingWindowIds.has(windowId)) { + await toPromise(this.onDidReloadingChange.event); + } + } +} + +export async function serve( + windowServer: IPCServer, + handle: string, + instantiationService: IInstantiationService +): TPromise { + const driver = instantiationService.createInstance(Driver, windowServer); + + const windowDriverRegistryChannel = new WindowDriverRegistryChannel(driver); + windowServer.registerChannel('windowDriverRegistry', windowDriverRegistryChannel); + + const server = await serveNet(handle); + const channel = new DriverChannel(driver); + server.registerChannel('driver', channel); + + return combinedDisposable([server, windowServer]); +} \ No newline at end of file diff --git a/src/vs/platform/driver/node/driver.ts b/src/vs/platform/driver/node/driver.ts new file mode 100644 index 00000000000..fb61d1d1fa6 --- /dev/null +++ b/src/vs/platform/driver/node/driver.ts @@ -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. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { TPromise } from 'vs/base/common/winjs.base'; +import { IDriver, DriverChannelClient } from 'vs/platform/driver/common/driver'; +import { connect as connectNet, Client } from 'vs/base/parts/ipc/node/ipc.net'; + +export async function connect(handle: string): TPromise<{ client: Client, driver: IDriver }> { + const client = await connectNet(handle, 'driverClient'); + const channel = client.getChannel('driver'); + const driver = new DriverChannelClient(channel); + return { client, driver }; +} diff --git a/src/vs/platform/environment/common/environment.ts b/src/vs/platform/environment/common/environment.ts index 5917f324e14..7b9d2d42259 100644 --- a/src/vs/platform/environment/common/environment.ts +++ b/src/vs/platform/environment/common/environment.ts @@ -57,6 +57,7 @@ export interface ParsedArgs { 'file-write'?: boolean; 'file-chmod'?: boolean; 'upload-logs'?: string; + 'driver'?: string; } export const IEnvironmentService = createDecorator('environmentService'); @@ -130,4 +131,6 @@ export interface IEnvironmentService { installSourcePath: string; disableUpdates: boolean; disableCrashReporter: boolean; + + driverHandle: string; } diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts index ed297cda684..7f14ec4ce06 100644 --- a/src/vs/platform/environment/node/argv.ts +++ b/src/vs/platform/environment/node/argv.ts @@ -29,7 +29,8 @@ const options: minimist.Opts = { 'enable-proposed-api', 'export-default-configuration', 'install-source', - 'upload-logs' + 'upload-logs', + 'driver' ], boolean: [ 'help', @@ -153,7 +154,7 @@ const extensionsHelp: { [name: string]: string; } = { '--show-versions': localize('showVersions', "Show versions of installed extensions, when using --list-extension."), '--install-extension ( | )': localize('installExtension', "Installs an extension."), '--uninstall-extension ( | )': localize('uninstallExtension', "Uninstalls an extension."), - '--enable-proposed-api ': localize('experimentalApis', "Enables proposed api features for an extension.") + '--enable-proposed-api ': localize('experimentalApis', "Enables proposed API features for an extension.") }; const troubleshootingHelp: { [name: string]: string; } = { @@ -163,8 +164,8 @@ const troubleshootingHelp: { [name: string]: string; } = { '-p, --performance': localize('performance', "Start with the 'Developer: Startup Performance' command enabled."), '--prof-startup': localize('prof-startup', "Run CPU profiler during startup"), '--disable-extensions': localize('disableExtensions', "Disable all installed extensions."), - '--inspect-extensions': localize('inspect-extensions', "Allow debugging and profiling of extensions. Check the developer tools for the connection uri."), - '--inspect-brk-extensions': localize('inspect-brk-extensions', "Allow debugging and profiling of extensions with the extension host being paused after start. Check the developer tools for the connection uri."), + '--inspect-extensions': localize('inspect-extensions', "Allow debugging and profiling of extensions. Check the developer tools for the connection URI."), + '--inspect-brk-extensions': localize('inspect-brk-extensions', "Allow debugging and profiling of extensions with the extension host being paused after start. Check the developer tools for the connection URI."), '--disable-gpu': localize('disableGPU', "Disable GPU hardware acceleration."), '--upload-logs': localize('uploadLogs', "Uploads logs from current session to a secure endpoint."), '--max-memory': localize('maxMemory', "Max memory size for a window (in Mbytes).") diff --git a/src/vs/platform/environment/node/environmentService.ts b/src/vs/platform/environment/node/environmentService.ts index cc1adabd5fd..cd875edaccd 100644 --- a/src/vs/platform/environment/node/environmentService.ts +++ b/src/vs/platform/environment/node/environmentService.ts @@ -170,6 +170,8 @@ export class EnvironmentService implements IEnvironmentService { get disableUpdates(): boolean { return !!this._args['disable-updates']; } get disableCrashReporter(): boolean { return !!this._args['disable-crash-reporter']; } + get driverHandle(): string { return this._args['driver']; } + constructor(private _args: ParsedArgs, private _execPath: string) { if (!process.env['VSCODE_LOGS']) { const key = toLocalISOString(new Date()).replace(/-|:|\.\d+Z$/g, ''); diff --git a/src/vs/platform/extensionManagement/common/extensionEnablementService.ts b/src/vs/platform/extensionManagement/common/extensionEnablementService.ts index 945ae66a841..28c6ce6805f 100644 --- a/src/vs/platform/extensionManagement/common/extensionEnablementService.ts +++ b/src/vs/platform/extensionManagement/common/extensionEnablementService.ts @@ -78,7 +78,13 @@ export class ExtensionEnablementService implements IExtensionEnablementService { } canChangeEnablement(extension: ILocalExtension): boolean { - return !this.environmentService.disableExtensions && !(extension.manifest && extension.manifest.contributes && extension.manifest.contributes.localizations && extension.manifest.contributes.localizations.length); + if (extension.manifest && extension.manifest.contributes && extension.manifest.contributes.localizations && extension.manifest.contributes.localizations.length) { + return false; + } + if (extension.type === LocalExtensionType.User && this.environmentService.disableExtensions) { + return false; + } + return true; } setEnablement(arg: ILocalExtension | IExtensionIdentifier, newState: EnablementState): TPromise { diff --git a/src/vs/platform/extensionManagement/common/extensionManagement.ts b/src/vs/platform/extensionManagement/common/extensionManagement.ts index 128b7f6bca1..973f0262d75 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -117,6 +117,12 @@ export interface IExtensionManifest { activationEvents?: string[]; extensionDependencies?: string[]; contributes?: IExtensionContributions; + repository?: { + url: string; + }; + bugs?: { + url: string; + }; } export interface IGalleryExtensionProperties { diff --git a/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts b/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts index a1a00bc5cc6..7d1bd38ba36 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts @@ -69,9 +69,9 @@ export function getLocalExtensionTelemetryData(extension: ILocalExtension): any id: getGalleryExtensionIdFromLocal(extension), name: extension.manifest.name, galleryId: null, - publisherId: null, - publisherName: null, - publisherDisplayName: null, + publisherId: extension.metadata ? extension.metadata.publisherId : null, + publisherName: extension.manifest.publisher, + publisherDisplayName: extension.metadata ? extension.metadata.publisherDisplayName : null, dependencies: extension.manifest.extensionDependencies && extension.manifest.extensionDependencies.length > 0 }; } @@ -82,9 +82,9 @@ export function getLocalExtensionTelemetryData(extension: ILocalExtension): any "id" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "name": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "galleryId": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "publisherId": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" }, - "publisherName": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" }, - "publisherDisplayName": { "classification": "PublicPersonalData", "purpose": "FeatureInsight" }, + "publisherId": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "publisherName": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "publisherDisplayName": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "dependencies": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "${include}": [ "${GalleryExtensionTelemetryData2}" diff --git a/src/vs/platform/extensionManagement/node/extensionGalleryService.ts b/src/vs/platform/extensionManagement/node/extensionGalleryService.ts index 694f98da3e4..92c29975c2b 100644 --- a/src/vs/platform/extensionManagement/node/extensionGalleryService.ts +++ b/src/vs/platform/extensionManagement/node/extensionGalleryService.ts @@ -18,7 +18,7 @@ import { IPager } from 'vs/base/common/paging'; import { IRequestOptions, IRequestContext, download, asJson, asText } from 'vs/base/node/request'; import pkg from 'vs/platform/node/package'; import product from 'vs/platform/node/product'; -import { isVersionValid } from 'vs/platform/extensions/node/extensionValidator'; +import { isEngineValid } from 'vs/platform/extensions/node/extensionValidator'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { readFile } from 'vs/base/node/pfs'; import { writeFileAndFlushSync } from 'vs/base/node/extfs'; @@ -520,7 +520,7 @@ export class ExtensionGalleryService implements IExtensionGalleryService { } loadCompatibleVersion(extension: IGalleryExtension): TPromise { - if (extension.properties.engine && this.isEngineValid(extension.properties.engine)) { + if (extension.properties.engine && isEngineValid(extension.properties.engine)) { return TPromise.wrap(extension); } @@ -682,7 +682,7 @@ export class ExtensionGalleryService implements IExtensionGalleryService { if (!engine) { return null; } - if (this.isEngineValid(engine)) { + if (isEngineValid(engine)) { return TPromise.wrap(version); } } @@ -703,7 +703,7 @@ export class ExtensionGalleryService implements IExtensionGalleryService { .then(manifest => { const engine = manifest.engines.vscode; - if (!this.isEngineValid(engine)) { + if (!isEngineValid(engine)) { return this.getLastValidExtensionVersionReccursively(extension, versions.slice(1)); } @@ -713,11 +713,6 @@ export class ExtensionGalleryService implements IExtensionGalleryService { }); } - private isEngineValid(engine: string): boolean { - // TODO@joao: discuss with alex '*' doesn't seem to be a valid engine version - return engine === '*' || isVersionValid(pkg.version, engine); - } - private static hasExtensionByName(extensions: IGalleryExtension[], name: string): boolean { for (const extension of extensions) { if (`${extension.publisher}.${extension.name}` === name) { diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index 1eec2f7e57e..96e5006095a 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -12,7 +12,7 @@ import * as errors from 'vs/base/common/errors'; import { assign } from 'vs/base/common/objects'; import { toDisposable, Disposable } from 'vs/base/common/lifecycle'; import { flatten, distinct } from 'vs/base/common/arrays'; -import { extract, buffer } from 'vs/base/node/zip'; +import { extract, buffer, ExtractError } from 'vs/base/node/zip'; import { TPromise } from 'vs/base/common/winjs.base'; import { IExtensionManagementService, IExtensionGalleryService, ILocalExtension, @@ -22,7 +22,7 @@ import { IExtensionIdentifier, IReportedExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { getGalleryExtensionIdFromLocal, adoptToGalleryExtensionId, areSameExtensions, getGalleryExtensionId, groupByExtension, getMaliciousExtensionsSet, getLocalExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { getGalleryExtensionIdFromLocal, adoptToGalleryExtensionId, areSameExtensions, getGalleryExtensionId, groupByExtension, getMaliciousExtensionsSet, getLocalExtensionId, getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { localizeManifest } from '../common/extensionNls'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { Limiter, always } from 'vs/base/common/async'; @@ -37,6 +37,8 @@ import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import Severity from 'vs/base/common/severity'; import { ExtensionsLifecycle } from 'vs/platform/extensionManagement/node/extensionLifecycle'; import { toErrorMessage } from 'vs/base/common/errorMessage'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { isEngineValid } from 'vs/platform/extensions/node/extensionValidator'; const SystemExtensionsRoot = path.normalize(path.join(URI.parse(require.toUrl('')).fsPath, '..', 'extensions')); const ERROR_SCANNING_SYS_EXTENSIONS = 'scanningSystem'; @@ -48,8 +50,9 @@ const INSTALL_ERROR_VALIDATING = 'validating'; const INSTALL_ERROR_GALLERY = 'gallery'; const INSTALL_ERROR_LOCAL = 'local'; const INSTALL_ERROR_EXTRACTING = 'extracting'; +const INSTALL_ERROR_RENAMING = 'renaming'; const INSTALL_ERROR_DELETING = 'deleting'; -const INSTALL_ERROR_UNKNOWN = 'unknown'; +const ERROR_UNKNOWN = 'unknown'; export class ExtensionManagementError extends Error { constructor(message: string, readonly code: string) { @@ -108,6 +111,7 @@ export class ExtensionManagementService extends Disposable implements IExtension private uninstalledFileLimiter: Limiter; private reportedExtensions: TPromise | undefined; private lastReportTimestamp = 0; + private readonly installationStartTime: Map = new Map(); private readonly installingExtensions: Map> = new Map>(); private readonly manifestCache: ExtensionsManifestCache; private readonly extensionLifecycle: ExtensionsLifecycle; @@ -128,7 +132,8 @@ export class ExtensionManagementService extends Disposable implements IExtension @IEnvironmentService environmentService: IEnvironmentService, @IDialogService private dialogService: IDialogService, @IExtensionGalleryService private galleryService: IExtensionGalleryService, - @ILogService private logService: ILogService + @ILogService private logService: ILogService, + @ITelemetryService private telemetryService: ITelemetryService, ) { super(); this.extensionsPath = environmentService.extensionsPath; @@ -145,6 +150,9 @@ export class ExtensionManagementService extends Disposable implements IExtension return validateLocalExtension(zipPath) .then(manifest => { const identifier = { id: getLocalExtensionIdFromManifest(manifest) }; + if (manifest.engines && manifest.engines.vscode && !isEngineValid(manifest.engines.vscode)) { + return TPromise.wrapError(new Error(nls.localize('incompatible', "Unable to install Extension '{0}' as it is not compatible with Code '{1}'.", identifier.id, pkg.version))); + } return this.removeIfExists(identifier.id) .then( () => this.checkOutdated(manifest) @@ -205,7 +213,7 @@ export class ExtensionManagementService extends Disposable implements IExtension return this.getDependenciesToInstall(local.manifest.extensionDependencies) .then(dependenciesToInstall => this.downloadAndInstallExtensions(metadata ? dependenciesToInstall.filter(d => d.identifier.uuid !== metadata.id) : dependenciesToInstall)) .then(() => local, error => { - this.uninstallExtension(local); + this.setUninstalled(local); return TPromise.wrapError(new Error(nls.localize('errorInstallingDependencies', "Error while installing dependencies. {0}", error instanceof Error ? error.message : error))); }); } @@ -241,7 +249,7 @@ export class ExtensionManagementService extends Disposable implements IExtension return this.findGalleryExtension(extension) .then(galleryExtension => { if (galleryExtension) { - return this.uninstallExtension(extension) + return this.setUninstalled(extension) .then(() => this.removeUninstalledExtension(extension) .then( () => this.installFromGallery(galleryExtension), @@ -326,6 +334,7 @@ export class ExtensionManagementService extends Disposable implements IExtension private onInstallExtensions(extensions: IGalleryExtension[]): void { for (const extension of extensions) { this.logService.info('Installing extension:', extension.name); + this.installationStartTime.set(extension.identifier.id, new Date().getTime()); const id = getLocalExtensionIdFromGallery(extension, extension.version); this._onInstallExtension.fire({ identifier: { id, uuid: extension.identifier.uuid }, gallery: extension }); } @@ -340,10 +349,13 @@ export class ExtensionManagementService extends Disposable implements IExtension this.logService.info(`Extensions installed successfully:`, gallery.identifier.id); this._onDidInstallExtension.fire({ identifier, gallery, local }); } else { - const errorCode = error && (error).code ? (error).code : INSTALL_ERROR_UNKNOWN; + const errorCode = error && (error).code ? (error).code : ERROR_UNKNOWN; this.logService.error(`Failed to install extension:`, gallery.identifier.id, error ? error.message : errorCode); this._onDidInstallExtension.fire({ identifier, gallery, error: errorCode }); } + const startTime = this.installationStartTime.get(gallery.identifier.id); + this.reportTelemetry('extensionGallery:install', getGalleryExtensionTelemetryData(gallery), startTime ? new Date().getTime() - startTime : void 0, error); + this.installationStartTime.delete(gallery.identifier.id); }); return errors.length ? TPromise.wrapError(this.joinErrors(errors)) : TPromise.as(null); } @@ -401,10 +413,13 @@ export class ExtensionManagementService extends Disposable implements IExtension } private extractAndInstall({ zipPath, id, metadata }: InstallableExtension): TPromise { - const extractPath = path.join(this.extensionsPath, `.${id}`); // Extract to temp path - return this.extract(id, zipPath, extractPath, { sourcePath: 'extension', overwrite: true }) - .then(() => this.completeInstall(id, extractPath)) - .then(() => this.scanExtension(id, this.extensionsPath, LocalExtensionType.User)) + const tempPath = path.join(this.extensionsPath, `.${id}`); + const extensionPath = path.join(this.extensionsPath, id); + return this.extractAndRename(id, zipPath, tempPath, extensionPath) + .then(() => { + this.logService.info('Installation completed.', id); + return this.scanExtension(id, this.extensionsPath, LocalExtensionType.User); + }) .then(local => { if (metadata) { local.metadata = metadata; @@ -414,42 +429,45 @@ export class ExtensionManagementService extends Disposable implements IExtension }); } - private extract(id: string, zipPath: string, extractPath: string, options: any): TPromise { + private extractAndRename(id: string, zipPath: string, extractPath: string, renamePath: string): TPromise { + return this.extract(id, zipPath, extractPath) + .then(() => this.rename(id, extractPath, renamePath, Date.now() + (30 * 1000) /* Retry for 30 seconds */) + .then( + () => this.logService.info('Renamed to', renamePath), + e => { + this.logService.info('Rename failed. Deleting from extracted location', extractPath); + return always(pfs.rimraf(extractPath), () => null).then(() => TPromise.wrapError(e)); + })); + } + + private extract(id: string, zipPath: string, extractPath: string): TPromise { this.logService.trace(`Started extracting the extension from ${zipPath} to ${extractPath}`); return pfs.rimraf(extractPath) .then( - () => extract(zipPath, extractPath, options) + () => extract(zipPath, extractPath, { sourcePath: 'extension', overwrite: true }) .then( () => this.logService.info(`Extracted extension to ${extractPath}:`, id), e => always(pfs.rimraf(extractPath), () => null) - .then(() => TPromise.wrapError(new ExtensionManagementError(e.message, INSTALL_ERROR_EXTRACTING)))), + .then(() => TPromise.wrapError(new ExtensionManagementError(e.message, e instanceof ExtractError ? e.type : INSTALL_ERROR_EXTRACTING)))), e => TPromise.wrapError(new ExtensionManagementError(this.joinErrors(e).message, INSTALL_ERROR_DELETING))); } - private completeInstall(id: string, extractPath: string): TPromise { - return this.rename(id, extractPath, Date.now() + (5 * 1000) /* Retry for 5 seconds */) - .then( - () => this.logService.info('Installation compelted.', id), - e => { - this.logService.info('Deleting the extracted extension', id); - return always(pfs.rimraf(extractPath), () => null) - .then(() => TPromise.wrapError(e)); - }); - } - - private rename(id: string, extractPath: string, retryUntil: number): TPromise { - return pfs.rename(extractPath, path.join(this.extensionsPath, id)) - .then(null, error => - isWindows && error && error.code === 'EPERM' && Date.now() < retryUntil - ? this.rename(id, extractPath, retryUntil) - : TPromise.wrapError(error)); + private rename(id: string, extractPath: string, renamePath: string, retryUntil: number): TPromise { + return pfs.rename(extractPath, renamePath) + .then(null, error => { + if (isWindows && error && error.code === 'EPERM' && Date.now() < retryUntil) { + this.logService.info(`Failed renaming ${extractPath} to ${renamePath} with 'EPERM' error. Trying again...`); + return this.rename(id, extractPath, renamePath, retryUntil); + } + return TPromise.wrapError(new ExtensionManagementError(error.message || nls.localize('renameError', "Unknown error while renaming {0} to {1}", extractPath, renamePath), error.code || INSTALL_ERROR_RENAMING)); + }); } private rollback(extensions: IGalleryExtension[]): TPromise { return this.getInstalled(LocalExtensionType.User) .then(installed => TPromise.join(installed.filter(local => extensions.some(galleryExtension => local.identifier.id === getLocalExtensionIdFromGallery(galleryExtension, galleryExtension.version))) // Only check id (pub.name-version) because we want to rollback the exact version - .map(local => this.uninstallExtension(local)))) + .map(local => this.setUninstalled(local)))) .then(() => null, () => null); } @@ -520,7 +538,7 @@ export class ExtensionManagementService extends Disposable implements IExtension .then(() => this.hasDependencies(extension, installed) ? this.promptForDependenciesAndUninstall(extension, installed, force) : this.promptAndUninstall(extension, installed, force)) .then(() => this.postUninstallExtension(extension), error => { - this.postUninstallExtension(extension, INSTALL_ERROR_LOCAL); + this.postUninstallExtension(extension, new ExtensionManagementError(error instanceof Error ? error.message : error, INSTALL_ERROR_LOCAL)); return TPromise.wrapError(error); }); } @@ -640,7 +658,7 @@ export class ExtensionManagementService extends Disposable implements IExtension .then(() => this.uninstallExtension(extension)) .then(() => this.postUninstallExtension(extension), error => { - this.postUninstallExtension(extension, INSTALL_ERROR_LOCAL); + this.postUninstallExtension(extension, new ExtensionManagementError(error instanceof Error ? error.message : error, INSTALL_ERROR_LOCAL)); return TPromise.wrapError(error); }); } @@ -655,12 +673,14 @@ export class ExtensionManagementService extends Disposable implements IExtension } private uninstallExtension(local: ILocalExtension): TPromise { - return this.setUninstalled(local.identifier.id); + // Set all versions of the extension as uninstalled + return this.scanUserExtensions(false) + .then(userExtensions => this.setUninstalled(...userExtensions.filter(u => areSameExtensions({ id: getGalleryExtensionIdFromLocal(u), uuid: u.identifier.uuid }, { id: getGalleryExtensionIdFromLocal(local), uuid: local.identifier.uuid })))); } - private async postUninstallExtension(extension: ILocalExtension, error?: string): TPromise { + private async postUninstallExtension(extension: ILocalExtension, error?: Error): TPromise { if (error) { - this.logService.error('Failed to uninstall extension:', extension.identifier.id, error); + this.logService.error('Failed to uninstall extension:', extension.identifier.id, error.message); } else { this.logService.info('Successfully uninstalled extension:', extension.identifier.id); // only report if extension has a mapped gallery extension. UUID identifies the gallery extension. @@ -668,7 +688,9 @@ export class ExtensionManagementService extends Disposable implements IExtension await this.galleryService.reportStatistic(extension.manifest.publisher, extension.manifest.name, extension.manifest.version, StatisticType.Uninstall); } } - this._onDidUninstallExtension.fire({ identifier: extension.identifier, error }); + this.reportTelemetry('extensionGallery:uninstall', getLocalExtensionTelemetryData(extension), void 0, error); + const errorcode = error ? error instanceof ExtensionManagementError ? error.code : ERROR_UNKNOWN : void 0; + this._onDidUninstallExtension.fire({ identifier: extension.identifier, error: errorcode }); } getInstalled(type: LocalExtensionType = null): TPromise { @@ -791,7 +813,8 @@ export class ExtensionManagementService extends Disposable implements IExtension }); } - private setUninstalled(...ids: string[]): TPromise { + private setUninstalled(...extensions: ILocalExtension[]): TPromise { + const ids = extensions.map(e => e.identifier.id); return this.withUninstalledExtensions(uninstalled => assign(uninstalled, ids.reduce((result, id) => { result[id] = true; return result; }, {}))); } @@ -845,6 +868,31 @@ export class ExtensionManagementService extends Disposable implements IExtension return []; }); } + + private reportTelemetry(eventName: string, extensionData: any, duration: number, error?: Error): void { + const errorcode = error ? error instanceof ExtensionManagementError ? error.code : ERROR_UNKNOWN : void 0; + /* __GDPR__ + "extensionGallery:install" : { + "success": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, + "duration" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, + "errorcode": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, + "${include}": [ + "${GalleryExtensionTelemetryData}" + ] + } + */ + /* __GDPR__ + "extensionGallery:uninstall" : { + "success": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, + "duration" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, + "errorcode": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, + "${include}": [ + "${GalleryExtensionTelemetryData}" + ] + } + */ + this.telemetryService.publicLog(eventName, assign(extensionData, { success: !error, duration, errorcode })); + } } export function getLocalExtensionIdFromGallery(extension: IGalleryExtension, version: string): string { diff --git a/src/vs/platform/extensionManagement/test/common/extensionEnablementService.test.ts b/src/vs/platform/extensionManagement/test/common/extensionEnablementService.test.ts index f4feeba2973..717a17dd3b0 100644 --- a/src/vs/platform/extensionManagement/test/common/extensionEnablementService.test.ts +++ b/src/vs/platform/extensionManagement/test/common/extensionEnablementService.test.ts @@ -6,7 +6,7 @@ import * as assert from 'assert'; import * as sinon from 'sinon'; -import { IExtensionManagementService, IExtensionEnablementService, DidUninstallExtensionEvent, EnablementState, IExtensionContributions, ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionManagementService, IExtensionEnablementService, DidUninstallExtensionEvent, EnablementState, IExtensionContributions, ILocalExtension, LocalExtensionType } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionEnablementService'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { Emitter } from 'vs/base/common/event'; @@ -324,6 +324,20 @@ suite('ExtensionEnablementService Test', () => { test('test canChangeEnablement return false for language packs', () => { assert.equal(testObject.canChangeEnablement(aLocalExtension('pub.a', { localizations: [{ languageId: 'gr', translations: [{ id: 'vscode', path: 'path' }] }] })), false); }); + + test('test canChangeEnablement return false when extensions are disabled in environment', () => { + instantiationService.stub(IEnvironmentService, { disableExtensions: true } as IEnvironmentService); + testObject = new TestExtensionEnablementService(instantiationService); + assert.equal(testObject.canChangeEnablement(aLocalExtension('pub.a')), false); + }); + + test('test canChangeEnablement return true for system extensions when extensions are disabled in environment', () => { + instantiationService.stub(IEnvironmentService, { disableExtensions: true } as IEnvironmentService); + testObject = new TestExtensionEnablementService(instantiationService); + const extension = aLocalExtension('pub.a'); + extension.type = LocalExtensionType.System; + assert.equal(testObject.canChangeEnablement(extension), true); + }); }); function aLocalExtension(id: string, contributes?: IExtensionContributions): ILocalExtension { @@ -334,6 +348,7 @@ function aLocalExtension(id: string, contributes?: IExtensionContributions): ILo name, publisher, contributes - } + }, + type: LocalExtensionType.User }); } diff --git a/src/vs/platform/extensions/node/extensionValidator.ts b/src/vs/platform/extensions/node/extensionValidator.ts index 607507a7c44..a500010b134 100644 --- a/src/vs/platform/extensions/node/extensionValidator.ts +++ b/src/vs/platform/extensions/node/extensionValidator.ts @@ -5,6 +5,7 @@ 'use strict'; import * as nls from 'vs/nls'; +import pkg from 'vs/platform/node/package'; export interface IParsedVersion { hasCaret: boolean; @@ -219,11 +220,16 @@ export function isValidExtensionVersion(version: string, extensionDesc: IReduced return isVersionValid(version, extensionDesc.engines.vscode, notices); } +export function isEngineValid(engine: string): boolean { + // TODO@joao: discuss with alex '*' doesn't seem to be a valid engine version + return engine === '*' || isVersionValid(pkg.version, engine); +} + export function isVersionValid(currentVersion: string, requestedVersion: string, notices: string[] = []): boolean { let desiredVersion = normalizeVersion(parseVersion(requestedVersion)); if (!desiredVersion) { - notices.push(nls.localize('versionSyntax', "Could not parse `engines.vscode` value {0}. Please use, for example: ^0.10.0, ^1.2.3, ^0.11.0, ^0.10.x, etc.", requestedVersion)); + notices.push(nls.localize('versionSyntax', "Could not parse `engines.vscode` value {0}. Please use, for example: ^1.22.0, ^1.22.x, etc.", requestedVersion)); return false; } diff --git a/src/vs/platform/files/common/files.ts b/src/vs/platform/files/common/files.ts index f3f03d75732..e9f5c6e78d6 100644 --- a/src/vs/platform/files/common/files.ts +++ b/src/vs/platform/files/common/files.ts @@ -11,17 +11,25 @@ import * as glob from 'vs/base/common/glob'; import { isLinux } from 'vs/base/common/platform'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { Event } from 'vs/base/common/event'; -import { beginsWithIgnoreCase } from 'vs/base/common/strings'; -import { IProgress } from 'vs/platform/progress/common/progress'; +import { startsWithIgnoreCase } from 'vs/base/common/strings'; import { IDisposable } from 'vs/base/common/lifecycle'; import { isEqualOrParent, isEqual } from 'vs/base/common/resources'; import { isUndefinedOrNull } from 'vs/base/common/types'; export const IFileService = createDecorator('fileService'); +export interface IResourceEncodings { + getWriteEncoding(resource: URI, preferredEncoding?: string): string; +} + export interface IFileService { _serviceBrand: any; + /** + * Helper to determine read/write encoding for resources. + */ + encoding: IResourceEncodings; + /** * Allows to listen for file changes. The event will fire for every file within the opened workspace * (if any) as well as all files that have been watched explicitly using the #watchFileChanges() API. @@ -33,15 +41,20 @@ export interface IFileService { */ onAfterOperation: Event; + /** + * An event that is fired when a file system provider is added or removed + */ + onDidChangeFileSystemProviderRegistrations: Event; + /** * Registeres a file system provider for a certain scheme. */ - registerProvider?(scheme: string, provider: IFileSystemProvider): IDisposable; + registerProvider(scheme: string, provider: IFileSystemProvider): IDisposable; /** * Checks if this file service can handle the given resource. */ - canHandleResource?(resource: URI): boolean; + canHandleResource(resource: URI): boolean; /** * Resolve the properties of a file identified by the resource. @@ -120,23 +133,12 @@ export interface IFileService { */ rename(resource: URI, newName: string): TPromise; - /** - * Creates a new empty file if the given path does not exist and otherwise - * will set the mtime and atime of the file to the current date. - */ - touchFile(resource: URI): TPromise; - /** * Deletes the provided file. The optional useTrash parameter allows to * move the file to trash. */ del(resource: URI, useTrash?: boolean): TPromise; - /** - * Imports the file to the parent identified by the resource. - */ - importFile(source: URI, targetFolder: URI): TPromise; - /** * Allows to start a watcher that reports file change events on the provided resource. */ @@ -147,59 +149,97 @@ export interface IFileService { */ unwatchFileChanges(resource: URI): void; - /** - * Configures the file service with the provided options. - */ - updateOptions(options: object): void; - - /** - * Returns the preferred encoding to use for a given resource. - */ - getEncoding(resource: URI, preferredEncoding?: string): string; - /** * Frees up any resources occupied by this service. */ dispose(): void; } - -export enum FileType { - File = 0, - Dir = 1, - Symlink = 2 +export enum FileType2 { + File = 1, + Directory = 2, + SymbolicLink = 4, } + +export interface FileOptions { + /** + * Create a file when it doesn't exists. + */ + create?: boolean; + + /** + * In combination with [`create`](FileOptions.create) but + * the operation should fail when a file already exists. + */ + exclusive?: boolean; + + /** + * Open a file for reading. + */ + read?: boolean; + + /** + * Open a file for writing. + */ + write?: boolean; +} + export interface IStat { - id: number | string; + isFile: boolean; + isDirectory: boolean; + isSymbolicLink: boolean; mtime: number; size: number; - type: FileType; +} + +export interface IWatchOptions { + recursive?: boolean; + exclude?: string[]; +} + +export enum FileSystemProviderCapabilities { + FileReadWrite = 1 << 1, + FileOpenReadWriteClose = 1 << 2, + FileFolderCopy = 1 << 3, + + PathCaseSensitive = 1 << 10 } export interface IFileSystemProvider { - onDidChange?: Event; + readonly capabilities: FileSystemProviderCapabilities; + + onDidChangeFile: Event; + watch(resource: URI, opts: IWatchOptions): IDisposable; - // more... - // - utimes(resource: URI, mtime: number, atime: number): TPromise; stat(resource: URI): TPromise; - read(resource: URI, offset: number, count: number, progress: IProgress): TPromise; - write(resource: URI, content: Uint8Array): TPromise; - move(from: URI, to: URI): TPromise; mkdir(resource: URI): TPromise; - readdir(resource: URI): TPromise<[URI, IStat][]>; - rmdir(resource: URI): TPromise; - unlink(resource: URI): TPromise; + readdir(resource: URI): TPromise<[string, IStat][]>; + delete(resource: URI): TPromise; + + rename(from: URI, to: URI, opts: FileOptions): TPromise; + copy?(from: URI, to: URI, opts: FileOptions): TPromise; + + readFile?(resource: URI, opts: FileOptions): TPromise; + writeFile?(resource: URI, content: Uint8Array, opts: FileOptions): TPromise; + + open?(resource: URI, opts: FileOptions): TPromise; + close?(fd: number): TPromise; + read?(fd: number, pos: number, data: Uint8Array, offset: number, length: number): TPromise; + write?(fd: number, pos: number, data: Uint8Array, offset: number, length: number): TPromise; } +export interface IFileSystemProviderRegistrationEvent { + added: boolean; + scheme: string; + provider?: IFileSystemProvider; +} export enum FileOperation { CREATE, DELETE, MOVE, - COPY, - IMPORT + COPY } export class FileOperationEvent { @@ -348,29 +388,12 @@ export function isParent(path: string, candidate: string, ignoreCase?: boolean): } if (ignoreCase) { - return beginsWithIgnoreCase(path, candidate); + return startsWithIgnoreCase(path, candidate); } return path.indexOf(candidate) === 0; } -export function indexOf(path: string, candidate: string, ignoreCase?: boolean): number { - if (candidate.length > path.length) { - return -1; - } - - if (path === candidate) { - return 0; - } - - if (ignoreCase) { - path = path.toLowerCase(); - candidate = candidate.toLowerCase(); - } - - return path.indexOf(candidate); -} - export interface IBaseStat { /** @@ -472,6 +495,14 @@ export interface ITextSnapshot { read(): string; } +export class StringSnapshot implements ITextSnapshot { + constructor(private _value: string) { } + read(): string { + let ret = this._value; + this._value = null; + return ret; + } +} /** * Helper method to convert a snapshot into its full string form. */ @@ -510,9 +541,10 @@ export interface IResolveContentOptions { acceptTextOnly?: boolean; /** - * The optional etag parameter allows to return a 304 (Not Modified) if the etag matches - * with the remote resource. It is the task of the caller to makes sure to handle this - * error case from the promise. + * The optional etag parameter allows to return early from resolving the resource if + * the contents on disk match the etag. This prevents accumulated reading of resources + * that have been read already with the same etag. + * It is the task of the caller to makes sure to handle this error case from the promise. */ etag?: string; @@ -566,6 +598,11 @@ export interface IUpdateContentOptions { * The etag of the file. This can be used to prevent dirty writes. */ etag?: string; + + /** + * Run mkdirp before saving. + */ + mkdirp?: boolean; } export interface IResolveFileOptions { @@ -582,11 +619,6 @@ export interface ICreateFileOptions { overwrite?: boolean; } -export interface IImportResult { - stat: IFileStat; - isNew: boolean; -} - export class FileOperationError extends Error { constructor(message: string, public fileOperationResult: FileOperationResult, public options?: IResolveContentOptions & IUpdateContentOptions & ICreateFileOptions) { super(message); diff --git a/src/vs/platform/files/test/files.test.ts b/src/vs/platform/files/test/files.test.ts index 305db3647cd..aba5148b53d 100644 --- a/src/vs/platform/files/test/files.test.ts +++ b/src/vs/platform/files/test/files.test.ts @@ -8,7 +8,7 @@ import * as assert from 'assert'; import URI from 'vs/base/common/uri'; import { join, isEqual, isEqualOrParent } from 'vs/base/common/paths'; -import { FileChangeType, FileChangesEvent, isParent, indexOf } from 'vs/platform/files/common/files'; +import { FileChangeType, FileChangesEvent, isParent } from 'vs/platform/files/common/files'; import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; suite('Files', () => { @@ -187,16 +187,4 @@ suite('Files', () => { assert(!isEqualOrParent('foo/bar/test.ts', 'foo/BAR/test.', true)); } }); - - test('indexOf (ignorecase)', function () { - assert.equal(indexOf('/some/path', '/some/path', true), 0); - assert.equal(indexOf('/some/path/more', '/some/path', true), 0); - - assert.equal(indexOf('c:\\some\\path', 'c:\\some\\path', true), 0); - assert.equal(indexOf('c:\\some\\path\\more', 'c:\\some\\path', true), 0); - - assert.equal(indexOf('/some/path', '/some/other/path', true), -1); - - assert.equal(indexOf('/some/path', '/some/PATH', true), 0); - }); }); \ No newline at end of file diff --git a/src/vs/platform/integrity/node/integrityServiceImpl.ts b/src/vs/platform/integrity/node/integrityServiceImpl.ts index 34fd1e44a4f..5a45065e013 100644 --- a/src/vs/platform/integrity/node/integrityServiceImpl.ts +++ b/src/vs/platform/integrity/node/integrityServiceImpl.ts @@ -14,7 +14,7 @@ import URI from 'vs/base/common/uri'; import Severity from 'vs/base/common/severity'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; -import { INotificationService, PromptOption } from 'vs/platform/notification/common/notification'; +import { INotificationService } from 'vs/platform/notification/common/notification'; interface IStorageData { dontShowPrompt: boolean; @@ -82,26 +82,24 @@ export class IntegrityServiceImpl implements IIntegrityService { private _prompt(): void { const storedData = this._storage.get(); if (storedData && storedData.dontShowPrompt && storedData.commit === product.commit) { - // Do not prompt - return; + return; // Do not prompt } - const choices: PromptOption[] = [nls.localize('integrity.moreInformation', "More Information"), { label: nls.localize('integrity.dontShowAgain', "Don't Show Again") }]; - - this.notificationService.prompt(Severity.Warning, nls.localize('integrity.prompt', "Your {0} installation appears to be corrupt. Please reinstall.", product.nameShort), choices).then(choice => { - switch (choice) { - case 0 /* More Information */: - const uri = URI.parse(product.checksumFailMoreInfoUrl); - window.open(uri.toString(true)); - break; - case 1 /* Do not show again */: - this._storage.set({ - dontShowPrompt: true, - commit: product.commit - }); - break; - } - }); + this.notificationService.prompt( + Severity.Warning, + nls.localize('integrity.prompt', "Your {0} installation appears to be corrupt. Please reinstall.", product.nameShort), + [ + { + label: nls.localize('integrity.moreInformation', "More Information"), + run: () => window.open(URI.parse(product.checksumFailMoreInfoUrl).toString(true)) + }, + { + label: nls.localize('integrity.dontShowAgain', "Don't Show Again"), + isSecondary: true, + run: () => this._storage.set({ dontShowPrompt: true, commit: product.commit }) + } + ] + ); } public isPure(): Thenable { diff --git a/src/vs/platform/issue/common/issue.ts b/src/vs/platform/issue/common/issue.ts index 8955b50b076..228bcdfefb1 100644 --- a/src/vs/platform/issue/common/issue.ts +++ b/src/vs/platform/issue/common/issue.ts @@ -11,6 +11,15 @@ import { ILocalExtension } from 'vs/platform/extensionManagement/common/extensio export const IIssueService = createDecorator('issueService'); +export interface WindowStyles { + backgroundColor: string; + color: string; +} +export interface WindowData { + styles: WindowStyles; + zoomLevel: number; +} + export enum IssueType { Bug, PerformanceIssue, @@ -18,9 +27,7 @@ export enum IssueType { SettingsSearchIssue } -export interface IssueReporterStyles { - backgroundColor: string; - color: string; +export interface IssueReporterStyles extends WindowStyles { textLinkColor: string; inputBackground: string; inputForeground: string; @@ -35,9 +42,8 @@ export interface IssueReporterStyles { sliderActiveColor: string; } -export interface IssueReporterData { +export interface IssueReporterData extends WindowData { styles: IssueReporterStyles; - zoomLevel: number; enabledExtensions: ILocalExtension[]; issueType?: IssueType; } @@ -58,7 +64,18 @@ export interface ISettingsSearchIssueReporterData extends IssueReporterData { export interface IssueReporterFeatures { } +export interface ProcessExplorerStyles extends WindowStyles { + hoverBackground: string; + hoverForeground: string; + highlightForeground: string; +} + +export interface ProcessExplorerData extends WindowData { + styles: ProcessExplorerStyles; +} + export interface IIssueService { _serviceBrand: any; openReporter(data: IssueReporterData): TPromise; + openProcessExplorer(data: ProcessExplorerData): TPromise; } diff --git a/src/vs/platform/issue/common/issueIpc.ts b/src/vs/platform/issue/common/issueIpc.ts index 99d73534b5e..d804c6579fe 100644 --- a/src/vs/platform/issue/common/issueIpc.ts +++ b/src/vs/platform/issue/common/issueIpc.ts @@ -7,7 +7,7 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { IChannel } from 'vs/base/parts/ipc/common/ipc'; -import { IIssueService, IssueReporterData } from './issue'; +import { IIssueService, IssueReporterData, ProcessExplorerData } from './issue'; export interface IIssueChannel extends IChannel { call(command: 'openIssueReporter', arg: IssueReporterData): TPromise; @@ -23,6 +23,8 @@ export class IssueChannel implements IIssueChannel { switch (command) { case 'openIssueReporter': return this.service.openReporter(arg); + case 'openProcessExplorer': + return this.service.openProcessExplorer(arg); } return undefined; } @@ -37,4 +39,8 @@ export class IssueChannelClient implements IIssueService { openReporter(data: IssueReporterData): TPromise { return this.channel.call('openIssueReporter', data); } + + openProcessExplorer(data: ProcessExplorerData): TPromise { + return this.channel.call('openProcessExplorer', data); + } } \ No newline at end of file diff --git a/src/vs/platform/issue/electron-main/issueService.ts b/src/vs/platform/issue/electron-main/issueService.ts index ce01cabbc9e..1d589bf8f16 100644 --- a/src/vs/platform/issue/electron-main/issueService.ts +++ b/src/vs/platform/issue/electron-main/issueService.ts @@ -9,12 +9,12 @@ import { TPromise, Promise } from 'vs/base/common/winjs.base'; import { localize } from 'vs/nls'; import * as objects from 'vs/base/common/objects'; import { parseArgs } from 'vs/platform/environment/node/argv'; -import { IIssueService, IssueReporterData, IssueReporterFeatures } from 'vs/platform/issue/common/issue'; +import { IIssueService, IssueReporterData, IssueReporterFeatures, ProcessExplorerData } from 'vs/platform/issue/common/issue'; import { BrowserWindow, ipcMain, screen } from 'electron'; import { ILaunchService } from 'vs/code/electron-main/launch'; import { getPerformanceInfo, PerformanceInfo, getSystemInfo, SystemInfo } from 'vs/code/electron-main/diagnostics'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { isMacintosh } from 'vs/base/common/platform'; +import { isMacintosh, IProcessEnvironment } from 'vs/base/common/platform'; import { ILogService } from 'vs/platform/log/common/log'; const DEFAULT_BACKGROUND_COLOR = '#1E1E1E'; @@ -22,13 +22,15 @@ const DEFAULT_BACKGROUND_COLOR = '#1E1E1E'; export class IssueService implements IIssueService { _serviceBrand: any; _issueWindow: BrowserWindow; - _parentWindow: BrowserWindow; + _issueParentWindow: BrowserWindow; + _processExplorerWindow: BrowserWindow; constructor( private machineId: string, + private userEnv: IProcessEnvironment, @IEnvironmentService private environmentService: IEnvironmentService, @ILaunchService private launchService: ILaunchService, - @ILogService private logService: ILogService + @ILogService private logService: ILogService, ) { } openReporter(data: IssueReporterData): TPromise { @@ -45,11 +47,11 @@ export class IssueService implements IIssueService { }); ipcMain.on('workbenchCommand', (event, arg) => { - this._parentWindow.webContents.send('vscode:runAction', { id: arg, from: 'issueReporter' }); + this._issueParentWindow.webContents.send('vscode:runAction', { id: arg, from: 'issueReporter' }); }); - this._parentWindow = BrowserWindow.getFocusedWindow(); - const position = this.getWindowPosition(); + this._issueParentWindow = BrowserWindow.getFocusedWindow(); + const position = this.getWindowPosition(this._issueParentWindow, 800, 900); this._issueWindow = new BrowserWindow({ width: position.width, height: position.height, @@ -72,7 +74,54 @@ export class IssueService implements IIssueService { return TPromise.as(null); } - private getWindowPosition() { + openProcessExplorer(data: ProcessExplorerData): TPromise { + // Create as singleton + if (!this._processExplorerWindow) { + const position = this.getWindowPosition(BrowserWindow.getFocusedWindow(), 800, 300); + this._processExplorerWindow = new BrowserWindow({ + skipTaskbar: true, + resizable: true, + width: position.width, + height: position.height, + minWidth: 300, + minHeight: 200, + x: position.x, + y: position.y, + backgroundColor: data.styles.backgroundColor, + title: localize('processExplorer', "Process Explorer") + }); + + this._processExplorerWindow.setMenuBarVisibility(false); + + const windowConfiguration = { + appRoot: this.environmentService.appRoot, + nodeCachedDataDir: this.environmentService.nodeCachedDataDir, + windowId: this._processExplorerWindow.id, + userEnv: this.userEnv, + machineId: this.machineId, + data + }; + + const environment = parseArgs(process.argv); + const config = objects.assign(environment, windowConfiguration); + for (let key in config) { + if (config[key] === void 0 || config[key] === null || config[key] === '') { + delete config[key]; // only send over properties that have a true value + } + } + + this._processExplorerWindow.loadURL(`${require.toUrl('vs/code/electron-browser/processExplorer/processExplorer.html')}?config=${encodeURIComponent(JSON.stringify(config))}`); + + this._processExplorerWindow.on('close', () => this._processExplorerWindow = void 0); + } + + // Focus + this._processExplorerWindow.focus(); + + return TPromise.as(null); + } + + private getWindowPosition(parentWindow: BrowserWindow, defaultWidth: number, defaultHeight: number) { // We want the new window to open on the same display that the parent is in let displayToUse: Electron.Display; const displays = screen.getAllDisplays(); @@ -92,8 +141,8 @@ export class IssueService implements IIssueService { } // if we have a last active window, use that display for the new window - if (!displayToUse && this._parentWindow) { - displayToUse = screen.getDisplayMatching(this._parentWindow.getBounds()); + if (!displayToUse && parentWindow) { + displayToUse = screen.getDisplayMatching(parentWindow.getBounds()); } // fallback to primary display or first display @@ -103,8 +152,8 @@ export class IssueService implements IIssueService { } let state = { - width: 800, - height: 900, + width: defaultWidth, + height: defaultHeight, x: undefined, y: undefined }; @@ -171,6 +220,7 @@ export class IssueService implements IIssueService { nodeCachedDataDir: this.environmentService.nodeCachedDataDir, windowId: this._issueWindow.id, machineId: this.machineId, + userEnv: this.userEnv, data, features }; diff --git a/src/vs/platform/keybinding/common/usLayoutResolvedKeybinding.ts b/src/vs/platform/keybinding/common/usLayoutResolvedKeybinding.ts index 78861625e1a..a0c923e8078 100644 --- a/src/vs/platform/keybinding/common/usLayoutResolvedKeybinding.ts +++ b/src/vs/platform/keybinding/common/usLayoutResolvedKeybinding.ts @@ -21,8 +21,7 @@ export class USLayoutResolvedKeybinding extends ResolvedKeybinding { super(); this._os = OS; if (actual === null) { - this._firstPart = null; - this._chordPart = null; + throw new Error(`Invalid USLayoutResolvedKeybinding`); } else if (actual.type === KeybindingType.Chord) { this._firstPart = actual.firstPart; this._chordPart = actual.chordPart; diff --git a/src/vs/platform/keybinding/test/common/abstractKeybindingService.test.ts b/src/vs/platform/keybinding/test/common/abstractKeybindingService.test.ts index 6c00827eac5..44ea5de8c5d 100644 --- a/src/vs/platform/keybinding/test/common/abstractKeybindingService.test.ts +++ b/src/vs/platform/keybinding/test/common/abstractKeybindingService.test.ts @@ -19,7 +19,7 @@ import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKe import { OS } from 'vs/base/common/platform'; import { IKeyboardEvent } from 'vs/platform/keybinding/common/keybinding'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; -import { INotificationService, NoOpNotification, INotification } from 'vs/platform/notification/common/notification'; +import { INotificationService, NoOpNotification, INotification, IPromptChoice } from 'vs/platform/notification/common/notification'; function createContext(ctx: any) { return { @@ -139,8 +139,8 @@ suite('AbstractKeybindingService', () => { showMessageCalls.push({ sev: Severity.Error, message }); return new NoOpNotification(); }, - prompt: () => { - return TPromise.as(0); + prompt(severity: Severity, message: string, choices: IPromptChoice[], onCancel?: () => void) { + throw new Error('not implemented'); } }; diff --git a/src/vs/platform/keybinding/test/common/mockKeybindingService.ts b/src/vs/platform/keybinding/test/common/mockKeybindingService.ts index 99c88c4fa06..8a44c7bd8db 100644 --- a/src/vs/platform/keybinding/test/common/mockKeybindingService.ts +++ b/src/vs/platform/keybinding/test/common/mockKeybindingService.ts @@ -120,4 +120,8 @@ export class MockKeybindingService implements IKeybindingService { public softDispatch(keybinding: IKeyboardEvent, target: IContextKeyServiceTarget): IResolveResult { return null; } + + dispatchEvent(e: IKeyboardEvent, target: IContextKeyServiceTarget): boolean { + return false; + } } diff --git a/src/vs/platform/lifecycle/electron-main/lifecycleMain.ts b/src/vs/platform/lifecycle/electron-main/lifecycleMain.ts index 5cc5c24c996..a2388d8fa49 100644 --- a/src/vs/platform/lifecycle/electron-main/lifecycleMain.ts +++ b/src/vs/platform/lifecycle/electron-main/lifecycleMain.ts @@ -14,7 +14,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' import { ICodeWindow } from 'vs/platform/windows/electron-main/windows'; import { ReadyState } from 'vs/platform/windows/common/windows'; import { handleVetos } from 'vs/platform/lifecycle/common/lifecycle'; -import { isMacintosh } from 'vs/base/common/platform'; +import { isMacintosh, isWindows } from 'vs/base/common/platform'; export const ILifecycleService = createDecorator('lifecycleService'); @@ -389,6 +389,19 @@ export class LifecycleService implements ILifecycleService { app.once('quit', () => { if (!vetoed) { this.stateService.setItem(LifecycleService.QUIT_FROM_RESTART_MARKER, true); + + // Windows: we are about to restart and as such we need to restore the original + // current working directory we had on startup to get the exact same startup + // behaviour. As such, we briefly change back to the VSCODE_CWD and then when + // Code starts it will set it back to the installation directory again. + try { + if (isWindows) { + process.chdir(process.env['VSCODE_CWD']); + } + } catch (err) { + this.logService.error(err); + } + app.relaunch({ args }); } }); diff --git a/src/vs/platform/log/node/spdlogService.ts b/src/vs/platform/log/node/spdlogService.ts index 84d7417db5e..5d579767204 100644 --- a/src/vs/platform/log/node/spdlogService.ts +++ b/src/vs/platform/log/node/spdlogService.ts @@ -7,13 +7,15 @@ import * as path from 'path'; import { ILogService, LogLevel, NullLogService, AbstractLogService } from 'vs/platform/log/common/log'; -import { RotatingLogger, setAsyncMode } from 'spdlog'; +import * as spdlog from 'spdlog'; export function createSpdLogService(processName: string, logLevel: LogLevel, logsFolder: string): ILogService { + // Do not crash if spdlog cannot be loaded try { - setAsyncMode(8192, 2000); + const _spdlog: typeof spdlog = require.__$__nodeRequire('spdlog'); + _spdlog.setAsyncMode(8192, 2000); const logfilePath = path.join(logsFolder, `${processName}.log`); - const logger = new RotatingLogger(processName, logfilePath, 1024 * 1024 * 5, 6); + const logger = new _spdlog.RotatingLogger(processName, logfilePath, 1024 * 1024 * 5, 6); logger.setLevel(0); return new SpdLogService(logger, logLevel); @@ -28,7 +30,7 @@ class SpdLogService extends AbstractLogService implements ILogService { _serviceBrand: any; constructor( - private readonly logger: RotatingLogger, + private readonly logger: spdlog.RotatingLogger, level: LogLevel = LogLevel.Error ) { super(); diff --git a/src/vs/platform/notification/common/notification.ts b/src/vs/platform/notification/common/notification.ts index 62b7e6751cd..4b1315c077f 100644 --- a/src/vs/platform/notification/common/notification.ts +++ b/src/vs/platform/notification/common/notification.ts @@ -7,10 +7,8 @@ import BaseSeverity from 'vs/base/common/severity'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IDisposable } from 'vs/base/common/lifecycle'; import { IAction } from 'vs/base/common/actions'; import { Event, Emitter } from 'vs/base/common/event'; -import { TPromise } from 'vs/base/common/winjs.base'; export import Severity = BaseSeverity; @@ -90,12 +88,12 @@ export interface INotificationProgress { done(): void; } -export interface INotificationHandle extends IDisposable { +export interface INotificationHandle { /** - * Will be fired once the notification is disposed. + * Will be fired once the notification is closed. */ - readonly onDidDispose: Event; + readonly onDidClose: Event; /** * Allows to indicate progress on the notification even after the @@ -119,28 +117,37 @@ export interface INotificationHandle extends IDisposable { * notification is already visible. */ updateActions(actions?: INotificationActions): void; + + /** + * Hide the notification and remove it from the notification center. + */ + close(): void; } +export interface IPromptChoice { -/** - * Primary choices show up as buttons in the notification below the message. - */ -export type PrimaryPromptChoice = string; - -/** - * Secondary choices show up under the gear icon in the header of the notification. - */ -export interface SecondaryPromptChoice { + /** + * Label to show for the choice to the user. + */ label: string; /** - * Wether to keep the notification open after the secondary choice was selected + * Primary choices show up as buttons in the notification below the message. + * Secondary choices show up under the gear icon in the header of the notification. + */ + isSecondary?: boolean; + + /** + * Wether to keep the notification open after the choice was selected * by the user. By default, will close the notification upon click. */ keepOpen?: boolean; -} -export type PromptOption = PrimaryPromptChoice | SecondaryPromptChoice; + /** + * Triggered when the user selects the choice. + */ + run: () => void; +} /** * A service to bring up notifications and non-modal prompts. @@ -185,27 +192,29 @@ export interface INotificationService { * Shows a prompt in the notification area with the provided choices. The prompt * is non-modal. If you want to show a modal dialog instead, use `IDialogService`. * - * @returns a promise that will resolve to the index of the choice that was picked. - * The promise can be cancelled to hide the notification prompt. + * @param onCancel will be called if the user closed the notification without picking + * any of the provided choices. + * + * @returns a handle on the notification to e.g. hide it or update message, buttons, etc. */ - prompt(severity: Severity, message: string, choices: PromptOption[]): TPromise; + prompt(severity: Severity, message: string, choices: IPromptChoice[], onCancel?: () => void): INotificationHandle; } export class NoOpNotification implements INotificationHandle { readonly progress = new NoOpProgress(); - private readonly _onDidDispose: Emitter = new Emitter(); + private readonly _onDidClose: Emitter = new Emitter(); - public get onDidDispose(): Event { - return this._onDidDispose.event; + public get onDidClose(): Event { + return this._onDidClose.event; } updateSeverity(severity: Severity): void { } updateMessage(message: NotificationMessage): void { } updateActions(actions?: INotificationActions): void { } - dispose(): void { - this._onDidDispose.dispose(); + close(): void { + this._onDidClose.dispose(); } } diff --git a/src/vs/platform/notification/test/common/testNotificationService.ts b/src/vs/platform/notification/test/common/testNotificationService.ts new file mode 100644 index 00000000000..d2d4021d653 --- /dev/null +++ b/src/vs/platform/notification/test/common/testNotificationService.ts @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { INotificationService, INotificationHandle, NoOpNotification, Severity, INotification, IPromptChoice } from 'vs/platform/notification/common/notification'; + +export class TestNotificationService implements INotificationService { + + public _serviceBrand: any; + + private static readonly NO_OP: INotificationHandle = new NoOpNotification(); + + public info(message: string): INotificationHandle { + return this.notify({ severity: Severity.Info, message }); + } + + public warn(message: string): INotificationHandle { + return this.notify({ severity: Severity.Warning, message }); + } + + public error(error: string | Error): INotificationHandle { + return this.notify({ severity: Severity.Error, message: error }); + } + + public notify(notification: INotification): INotificationHandle { + return TestNotificationService.NO_OP; + } + + public prompt(severity: Severity, message: string, choices: IPromptChoice[], onCancel?: () => void): INotificationHandle { + return TestNotificationService.NO_OP; + } +} \ No newline at end of file diff --git a/src/vs/platform/progress/common/progress.ts b/src/vs/platform/progress/common/progress.ts index 69d8ce1d430..25fd7b3d97a 100644 --- a/src/vs/platform/progress/common/progress.ts +++ b/src/vs/platform/progress/common/progress.ts @@ -80,7 +80,7 @@ export interface IProgressOptions { export interface IProgressStep { message?: string; - percentage?: number; + increment?: number; } export const IProgressService2 = createDecorator('progressService2'); diff --git a/src/vs/platform/quickOpen/common/quickOpen.ts b/src/vs/platform/quickOpen/common/quickOpen.ts index 24975fa0e09..bb886241cf7 100644 --- a/src/vs/platform/quickOpen/common/quickOpen.ts +++ b/src/vs/platform/quickOpen/common/quickOpen.ts @@ -33,6 +33,7 @@ export interface IPickOpenEntry { run?: (context: IEntryRunContext) => void; action?: IAction; payload?: any; + picked?: boolean; } export interface IPickOpenItem { @@ -84,6 +85,11 @@ export interface IPickOptions { * a context key to set when this picker is active */ contextKey?: string; + + /** + * an optional flag to make this picker multi-select (honoured by extension API) + */ + canSelectMany?: boolean; } export interface IInputOptions { diff --git a/src/vs/platform/quickinput/common/quickInput.ts b/src/vs/platform/quickinput/common/quickInput.ts new file mode 100644 index 00000000000..c5e05dc6cd4 --- /dev/null +++ b/src/vs/platform/quickinput/common/quickInput.ts @@ -0,0 +1,19 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { IPickOptions, IPickOpenEntry } from 'vs/platform/quickOpen/common/quickOpen'; +import { CancellationToken } from 'vs/base/common/cancellation'; + +export const IQuickInputService = createDecorator('quickInputService'); + +export interface IQuickInputService { + + _serviceBrand: any; + + pick(picks: TPromise, options?: IPickOptions, token?: CancellationToken): TPromise; +} diff --git a/src/vs/platform/telemetry/browser/errorTelemetry.ts b/src/vs/platform/telemetry/browser/errorTelemetry.ts index abc5a96d207..ec06e2c07ee 100644 --- a/src/vs/platform/telemetry/browser/errorTelemetry.ts +++ b/src/vs/platform/telemetry/browser/errorTelemetry.ts @@ -12,22 +12,37 @@ import { IDisposable, toDisposable, dispose } from 'vs/base/common/lifecycle'; import * as Errors from 'vs/base/common/errors'; import { safeStringify } from 'vs/base/common/objects'; +/* __GDPR__FRAGMENT__ + "ErrorEvent" : { + "stack": { "classification": "CustomerContent", "purpose": "PerformanceAndHealth" }, + "message" : { "classification": "CustomerContent", "purpose": "PerformanceAndHealth" }, + "filename" : { "classification": "CustomerContent", "purpose": "PerformanceAndHealth" }, + "callstack": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, + "msg" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, + "file" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, + "line": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth", "isMeasurement": true }, + "column": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth", "isMeasurement": true }, + "uncaught_error_name": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, + "uncaught_error_msg": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, + "count": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth", "isMeasurement": true } + } + */ interface ErrorEvent { - stack: string; - message?: string; - filename?: string; + callstack: string; + msg?: string; + file?: string; line?: number; column?: number; - error?: { name: string; message: string; }; - + uncaught_error_name?: string; + uncaught_error_msg?: string; count?: number; } namespace ErrorEvent { export function compare(a: ErrorEvent, b: ErrorEvent) { - if (a.stack < b.stack) { + if (a.callstack < b.callstack) { return -1; - } else if (a.stack > b.stack) { + } else if (a.callstack > b.callstack) { return 1; } return 0; @@ -89,32 +104,35 @@ export default class ErrorTelemetry { } // work around behavior in workerServer.ts that breaks up Error.stack - let stack = Array.isArray(err.stack) ? err.stack.join('\n') : err.stack; - let message = err.message ? err.message : safeStringify(err); + let callstack = Array.isArray(err.stack) ? err.stack.join('\n') : err.stack; + let msg = err.message ? err.message : safeStringify(err); // errors without a stack are not useful telemetry - if (!stack) { + if (!callstack) { return; } - this._enqueue({ message, stack }); + this._enqueue({ msg, callstack }); } - private _onUncaughtError(message: string, filename: string, line: number, column?: number, err?: any): void { + private _onUncaughtError(msg: string, file: string, line: number, column?: number, err?: any): void { let data: ErrorEvent = { - stack: message, - message, - filename, + callstack: msg, + msg, + file, line, column }; if (err) { let { name, message, stack } = err; - data.error = { name, message }; + data.uncaught_error_name = name; + if (message) { + data.uncaught_error_msg = message; + } if (stack) { - data.stack = Array.isArray(err.stack) + data.callstack = Array.isArray(err.stack) ? err.stack = err.stack.join('\n') : err.stack; } @@ -145,18 +163,10 @@ export default class ErrorTelemetry { for (let error of this._buffer) { /* __GDPR__ "UnhandledError" : { - "filename" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, - "message" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, - "name": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, - "stack": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, - "id": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, - "line": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth", "isMeasurement": true }, - "column": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth", "isMeasurement": true }, - "count": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth", "isMeasurement": true } + "${include}": [ "${ErrorEvent}" ] } */ - // __GDPR__TODO__ what's the complete set of properties? - this._telemetryService.publicLog('UnhandledError', error); + this._telemetryService.publicLog('UnhandledError', error, true); } this._buffer.length = 0; } diff --git a/src/vs/platform/telemetry/common/telemetry.ts b/src/vs/platform/telemetry/common/telemetry.ts index ebfc8144e6e..41a1987e9c9 100644 --- a/src/vs/platform/telemetry/common/telemetry.ts +++ b/src/vs/platform/telemetry/common/telemetry.ts @@ -29,7 +29,7 @@ export interface ITelemetryService { * Sends a telemetry event that has been privacy approved. * Do not call this unless you have been given approval. */ - publicLog(eventName: string, data?: ITelemetryData): TPromise; + publicLog(eventName: string, data?: ITelemetryData, anonymizeFilePaths?: boolean): TPromise; getTelemetryInfo(): TPromise; diff --git a/src/vs/platform/telemetry/common/telemetryService.ts b/src/vs/platform/telemetry/common/telemetryService.ts index 6dc744d8d44..dba27cde4b1 100644 --- a/src/vs/platform/telemetry/common/telemetryService.ts +++ b/src/vs/platform/telemetry/common/telemetryService.ts @@ -37,7 +37,7 @@ export class TelemetryService implements ITelemetryService { private _userOptIn: boolean; private _disposables: IDisposable[] = []; - private _cleanupPatterns: [RegExp, string][] = []; + private _cleanupPatterns: RegExp[] = []; constructor( config: ITelemetryServiceConfig, @@ -48,18 +48,11 @@ export class TelemetryService implements ITelemetryService { this._piiPaths = config.piiPaths || []; this._userOptIn = typeof config.userOptIn === 'undefined' ? true : config.userOptIn; - // static cleanup patterns for: - // #1 `file:///DANGEROUS/PATH/resources/app/Useful/Information` - // #2 // Any other file path that doesn't match the approved form above should be cleaned. - // #3 "Error: ENOENT; no such file or directory" is often followed with PII, clean it - this._cleanupPatterns.push( - [/file:\/\/\/.*?\/resources\/app\//gi, ''], - [/file:\/\/\/.*/gi, ''], - [/ENOENT: no such file or directory.*?\'([^\']+)\'/gi, 'ENOENT: no such file or directory'] - ); + // static cleanup pattern for: `file:///DANGEROUS/PATH/resources/app/Useful/Information` + this._cleanupPatterns = [/file:\/\/\/.*?\/resources\/app\//gi]; for (let piiPath of this._piiPaths) { - this._cleanupPatterns.push([new RegExp(escapeRegExpCharacters(piiPath), 'gi'), '']); + this._cleanupPatterns.push(new RegExp(escapeRegExpCharacters(piiPath), 'gi')); } if (this._configurationService) { @@ -98,7 +91,7 @@ export class TelemetryService implements ITelemetryService { this._disposables = dispose(this._disposables); } - publicLog(eventName: string, data?: ITelemetryData): TPromise { + publicLog(eventName: string, data?: ITelemetryData, anonymizeFilePaths?: boolean): TPromise { // don't send events when the user is optout if (!this._userOptIn) { return TPromise.as(undefined); @@ -112,7 +105,7 @@ export class TelemetryService implements ITelemetryService { // (last) remove all PII from data data = cloneAndChange(data, value => { if (typeof value === 'string') { - return this._cleanupInfo(value); + return this._cleanupInfo(value, anonymizeFilePaths); } return undefined; }); @@ -125,15 +118,41 @@ export class TelemetryService implements ITelemetryService { }); } - private _cleanupInfo(stack: string): string { + private _cleanupInfo(stack: string, anonymizeFilePaths?: boolean): string { + let updatedStack = stack; - // sanitize with configured cleanup patterns - for (let tuple of this._cleanupPatterns) { - let [regexp, replaceValue] = tuple; - stack = stack.replace(regexp, replaceValue); + if (anonymizeFilePaths) { + const cleanUpIndexes: [number, number][] = []; + for (let regexp of this._cleanupPatterns) { + while (true) { + const result = regexp.exec(stack); + if (!result) { + break; + } + cleanUpIndexes.push([result.index, regexp.lastIndex]); + } + } + + const nodeModulesRegex = /^[\\\/]?(node_modules|node_modules\.asar)[\\\/]/; + const fileRegex = /(file:\/\/)?([a-zA-Z]:(\\\\|\\|\/)|(\\\\|\\|\/))?([\w-\._]+(\\\\|\\|\/))+[\w-\._]*/g; + + while (true) { + const result = fileRegex.exec(stack); + if (!result) { + break; + } + // Anoynimize user file paths that do not need to be retained or cleaned up. + if (!nodeModulesRegex.test(result[0]) && cleanUpIndexes.every(([x, y]) => result.index < x || result.index >= y)) { + updatedStack = updatedStack.slice(0, result.index) + result[0].replace(/./g, 'a') + updatedStack.slice(fileRegex.lastIndex); + } + } } - return stack; + // sanitize with configured cleanup patterns + for (let regexp of this._cleanupPatterns) { + updatedStack = updatedStack.replace(regexp, ''); + } + return updatedStack; } } diff --git a/src/vs/platform/telemetry/node/commonProperties.ts b/src/vs/platform/telemetry/node/commonProperties.ts index 493d2692402..e997435d9c1 100644 --- a/src/vs/platform/telemetry/node/commonProperties.ts +++ b/src/vs/platform/telemetry/node/commonProperties.ts @@ -21,8 +21,6 @@ export function resolveCommonProperties(commit: string, version: string, machine result['version'] = version; // __GDPR__COMMON__ "common.platformVersion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } result['common.platformVersion'] = (os.release() || '').replace(/^(\d+)(\.\d+)?(\.\d+)?(.*)/, '$1$2$3'); - // __GDPR__COMMON__ "common.osVersion" : { "classification": "EndUserPseudonymizedInformation", "purpose": "FeatureInsight" } - result['common.osVersion'] = result['common.platformVersion']; // TODO: Drop this after the move to Nova // __GDPR__COMMON__ "common.platform" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } result['common.platform'] = Platform.Platform[Platform.platform]; // __GDPR__COMMON__ "common.nodePlatform" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } 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 2970c6b00bd..1c5e341b12b 100644 --- a/src/vs/platform/telemetry/test/electron-browser/commonProperties.test.ts +++ b/src/vs/platform/telemetry/test/electron-browser/commonProperties.test.ts @@ -48,7 +48,6 @@ suite('Telemetry - common properties', function () { // assert.ok('common.version.shell' in first.data); // only when running on electron // assert.ok('common.version.renderer' in first.data); - assert.ok('common.osVersion' in props, 'osVersion'); assert.ok('common.platformVersion' in props, 'platformVersion'); assert.ok('version' in props); assert.equal(props['common.source'], 'my.install.source'); 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 b29b0fff824..d0b935f401d 100644 --- a/src/vs/platform/telemetry/test/electron-browser/telemetryService.test.ts +++ b/src/vs/platform/telemetry/test/electron-browser/telemetryService.test.ts @@ -50,6 +50,10 @@ class ErrorTestingSettings { public noSuchFilePrefix: string; public noSuchFileMessage: string; public stack: string[]; + public randomUserFile: string = 'a/path/that/doe_snt/con-tain/code/names.js'; + public anonymizedRandomUserFile: string = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'; + public nodeModulePathToRetain: string = 'node_modules/path/that/shouldbe/retained/names.js:14:15854'; + public nodeModuleAsarPathToRetain: string = 'node_modules.asar/path/that/shouldbe/retained/names.js:14:12354'; constructor() { this.personalInfo = 'DANGEROUS/PATH'; @@ -64,20 +68,20 @@ class ErrorTestingSettings { this.noSuchFilePrefix = 'ENOENT: no such file or directory'; this.noSuchFileMessage = this.noSuchFilePrefix + ' \'' + this.personalInfo + '\''; - this.stack = ['at e._modelEvents (a/path/that/doesnt/contain/code/names.js:11:7309)', - ' at t.AllWorkers (a/path/that/doesnt/contain/code/names.js:6:8844)', - ' at e.(anonymous function) [as _modelEvents] (a/path/that/doesnt/contain/code/names.js:5:29552)', - ' at Function. (a/path/that/doesnt/contain/code/names.js:6:8272)', - ' at e.dispatch (a/path/that/doesnt/contain/code/names.js:5:26931)', - ' at e.request (a/path/that/doesnt/contain/code/names.js:14:1745)', - ' at t._handleMessage (another/path/that/doesnt/contain/code/names.js:14:17447)', - ' at t._onmessage (another/path/that/doesnt/contain/code/names.js:14:16976)', - ' at t.onmessage (another/path/that/doesnt/contain/code/names.js:14:15854)', - ' at DedicatedWorkerGlobalScope.self.onmessage', - this.dangerousPathWithImportantInfo, - this.dangerousPathWithoutImportantInfo, - this.missingModelMessage, - this.noSuchFileMessage]; + this.stack = [`at e._modelEvents (${this.randomUserFile}:11:7309)`, + ` at t.AllWorkers (${this.randomUserFile}:6:8844)`, + ` at e.(anonymous function) [as _modelEvents] (${this.randomUserFile}:5:29552)`, + ` at Function. (${this.randomUserFile}:6:8272)`, + ` at e.dispatch (${this.randomUserFile}:5:26931)`, + ` at e.request (/${this.nodeModuleAsarPathToRetain})`, + ` at t._handleMessage (${this.nodeModuleAsarPathToRetain})`, + ` at t._onmessage (/${this.nodeModulePathToRetain})`, + ` at t.onmessage (${this.nodeModulePathToRetain})`, + ` at DedicatedWorkerGlobalScope.self.onmessage`, + this.dangerousPathWithImportantInfo, + this.dangerousPathWithoutImportantInfo, + this.missingModelMessage, + this.noSuchFileMessage]; } } @@ -224,7 +228,7 @@ suite('TelemetryService', () => { this.clock.tick(ErrorTelemetry.ERROR_FLUSH_TIMEOUT); assert.equal(testAppender.getEventsCount(), 1); assert.equal(testAppender.events[0].eventName, 'UnhandledError'); - assert.equal(testAppender.events[0].data.message, 'This is a test.'); + assert.equal(testAppender.events[0].data.msg, 'This is a test.'); errorTelemetry.dispose(); service.dispose(); @@ -254,7 +258,7 @@ suite('TelemetryService', () => { // // assert.equal(testAppender.getEventsCount(), 1); // assert.equal(testAppender.events[0].eventName, 'UnhandledError'); - // assert.equal(testAppender.events[0].data.message, 'This should get logged'); + // assert.equal(testAppender.events[0].data.msg, 'This should get logged'); // // service.dispose(); // } finally { @@ -279,11 +283,33 @@ suite('TelemetryService', () => { assert.equal(testAppender.getEventsCount(), 1); assert.equal(testAppender.events[0].eventName, 'UnhandledError'); - assert.equal(testAppender.events[0].data.message, 'Error Message'); - assert.equal(testAppender.events[0].data.filename, 'file.js'); + assert.equal(testAppender.events[0].data.msg, 'Error Message'); + assert.equal(testAppender.events[0].data.file, 'file.js'); assert.equal(testAppender.events[0].data.line, 2); assert.equal(testAppender.events[0].data.column, 42); - assert.equal(testAppender.events[0].data.error.message, 'test'); + assert.equal(testAppender.events[0].data.uncaught_error_msg, 'test'); + + errorTelemetry.dispose(); + service.dispose(); + })); + + test('Error Telemetry removes PII from filename with spaces', sinon.test(function (this: any) { + let errorStub = sinon.stub(); + window.onerror = errorStub; + let settings = new ErrorTestingSettings(); + let testAppender = new TestTelemetryAppender(); + let service = new TelemetryService({ appender: testAppender }, undefined); + const errorTelemetry = new ErrorTelemetry(service); + + let personInfoWithSpaces = settings.personalInfo.slice(0, 2) + ' ' + settings.personalInfo.slice(2); + let dangerousFilenameError: any = new Error('dangerousFilename'); + dangerousFilenameError.stack = settings.stack; + (window.onerror)('dangerousFilename', settings.dangerousPathWithImportantInfo.replace(settings.personalInfo, personInfoWithSpaces) + '/test.js', 2, 42, dangerousFilenameError); + this.clock.tick(ErrorTelemetry.ERROR_FLUSH_TIMEOUT); + + assert.equal(errorStub.callCount, 1); + assert.equal(testAppender.events[0].data.file.indexOf(settings.dangerousPathWithImportantInfo.replace(settings.personalInfo, personInfoWithSpaces)), -1); + assert.equal(testAppender.events[0].data.file, settings.importantInfo + '/test.js'); errorTelemetry.dispose(); service.dispose(); @@ -303,7 +329,7 @@ suite('TelemetryService', () => { this.clock.tick(ErrorTelemetry.ERROR_FLUSH_TIMEOUT); assert.equal(errorStub.callCount, 1); - assert.equal(testAppender.events[0].data.filename.indexOf(settings.dangerousPathWithImportantInfo), -1); + assert.equal(testAppender.events[0].data.file.indexOf(settings.dangerousPathWithImportantInfo), -1); dangerousFilenameError = new Error('dangerousFilename'); dangerousFilenameError.stack = settings.stack; @@ -311,8 +337,8 @@ suite('TelemetryService', () => { this.clock.tick(ErrorTelemetry.ERROR_FLUSH_TIMEOUT); assert.equal(errorStub.callCount, 2); - assert.equal(testAppender.events[0].data.filename.indexOf(settings.dangerousPathWithImportantInfo), -1); - assert.equal(testAppender.events[0].data.filename, settings.importantInfo + '/test.js'); + assert.equal(testAppender.events[0].data.file.indexOf(settings.dangerousPathWithImportantInfo), -1); + assert.equal(testAppender.events[0].data.file, settings.importantInfo + '/test.js'); errorTelemetry.dispose(); service.dispose(); @@ -332,13 +358,13 @@ suite('TelemetryService', () => { Errors.onUnexpectedError(dangerousPathWithoutImportantInfoError); this.clock.tick(ErrorTelemetry.ERROR_FLUSH_TIMEOUT); - assert.equal(testAppender.events[0].data.message.indexOf(settings.personalInfo), -1); - assert.equal(testAppender.events[0].data.message.indexOf(settings.filePrefix), -1); + assert.equal(testAppender.events[0].data.msg.indexOf(settings.personalInfo), -1); + assert.equal(testAppender.events[0].data.msg.indexOf(settings.filePrefix), -1); - assert.equal(testAppender.events[0].data.stack.indexOf(settings.personalInfo), -1); - assert.equal(testAppender.events[0].data.stack.indexOf(settings.filePrefix), -1); - assert.notEqual(testAppender.events[0].data.stack.indexOf(settings.stack[4]), -1); - assert.equal(testAppender.events[0].data.stack.split('\n').length, settings.stack.length); + assert.equal(testAppender.events[0].data.callstack.indexOf(settings.personalInfo), -1); + assert.equal(testAppender.events[0].data.callstack.indexOf(settings.filePrefix), -1); + assert.notEqual(testAppender.events[0].data.callstack.indexOf(settings.stack[4].replace(settings.randomUserFile, settings.anonymizedRandomUserFile)), -1); + assert.equal(testAppender.events[0].data.callstack.split('\n').length, settings.stack.length); errorTelemetry.dispose(); service.dispose(); @@ -363,12 +389,12 @@ suite('TelemetryService', () => { assert.equal(errorStub.callCount, 1); // Test that no file information remains, esp. personal info - assert.equal(testAppender.events[0].data.message.indexOf(settings.personalInfo), -1); - assert.equal(testAppender.events[0].data.message.indexOf(settings.filePrefix), -1); - assert.equal(testAppender.events[0].data.stack.indexOf(settings.personalInfo), -1); - assert.equal(testAppender.events[0].data.stack.indexOf(settings.filePrefix), -1); - assert.notEqual(testAppender.events[0].data.stack.indexOf(settings.stack[4]), -1); - assert.equal(testAppender.events[0].data.stack.split('\n').length, settings.stack.length); + assert.equal(testAppender.events[0].data.msg.indexOf(settings.personalInfo), -1); + assert.equal(testAppender.events[0].data.msg.indexOf(settings.filePrefix), -1); + assert.equal(testAppender.events[0].data.callstack.indexOf(settings.personalInfo), -1); + assert.equal(testAppender.events[0].data.callstack.indexOf(settings.filePrefix), -1); + assert.notEqual(testAppender.events[0].data.callstack.indexOf(settings.stack[4].replace(settings.randomUserFile, settings.anonymizedRandomUserFile)), -1); + assert.equal(testAppender.events[0].data.callstack.split('\n').length, settings.stack.length); errorTelemetry.dispose(); service.dispose(); @@ -392,14 +418,14 @@ suite('TelemetryService', () => { Errors.onUnexpectedError(dangerousPathWithImportantInfoError); this.clock.tick(ErrorTelemetry.ERROR_FLUSH_TIMEOUT); - assert.notEqual(testAppender.events[0].data.message.indexOf(settings.importantInfo), -1); - assert.equal(testAppender.events[0].data.message.indexOf(settings.personalInfo), -1); - assert.equal(testAppender.events[0].data.message.indexOf(settings.filePrefix), -1); - assert.notEqual(testAppender.events[0].data.stack.indexOf(settings.importantInfo), -1); - assert.equal(testAppender.events[0].data.stack.indexOf(settings.personalInfo), -1); - assert.equal(testAppender.events[0].data.stack.indexOf(settings.filePrefix), -1); - assert.notEqual(testAppender.events[0].data.stack.indexOf(settings.stack[4]), -1); - assert.equal(testAppender.events[0].data.stack.split('\n').length, settings.stack.length); + assert.notEqual(testAppender.events[0].data.msg.indexOf(settings.importantInfo), -1); + assert.equal(testAppender.events[0].data.msg.indexOf(settings.personalInfo), -1); + assert.equal(testAppender.events[0].data.msg.indexOf(settings.filePrefix), -1); + assert.notEqual(testAppender.events[0].data.callstack.indexOf(settings.importantInfo), -1); + assert.equal(testAppender.events[0].data.callstack.indexOf(settings.personalInfo), -1); + assert.equal(testAppender.events[0].data.callstack.indexOf(settings.filePrefix), -1); + assert.notEqual(testAppender.events[0].data.callstack.indexOf(settings.stack[4].replace(settings.randomUserFile, settings.anonymizedRandomUserFile)), -1); + assert.equal(testAppender.events[0].data.callstack.split('\n').length, settings.stack.length); errorTelemetry.dispose(); service.dispose(); @@ -424,19 +450,75 @@ suite('TelemetryService', () => { assert.equal(errorStub.callCount, 1); // Test that important information remains but personal info does not - assert.notEqual(testAppender.events[0].data.message.indexOf(settings.importantInfo), -1); - assert.equal(testAppender.events[0].data.message.indexOf(settings.personalInfo), -1); - assert.equal(testAppender.events[0].data.message.indexOf(settings.filePrefix), -1); - assert.notEqual(testAppender.events[0].data.stack.indexOf(settings.importantInfo), -1); - assert.equal(testAppender.events[0].data.stack.indexOf(settings.personalInfo), -1); - assert.equal(testAppender.events[0].data.stack.indexOf(settings.filePrefix), -1); - assert.notEqual(testAppender.events[0].data.stack.indexOf(settings.stack[4]), -1); - assert.equal(testAppender.events[0].data.stack.split('\n').length, settings.stack.length); + assert.notEqual(testAppender.events[0].data.msg.indexOf(settings.importantInfo), -1); + assert.equal(testAppender.events[0].data.msg.indexOf(settings.personalInfo), -1); + assert.equal(testAppender.events[0].data.msg.indexOf(settings.filePrefix), -1); + assert.notEqual(testAppender.events[0].data.callstack.indexOf(settings.importantInfo), -1); + assert.equal(testAppender.events[0].data.callstack.indexOf(settings.personalInfo), -1); + assert.equal(testAppender.events[0].data.callstack.indexOf(settings.filePrefix), -1); + assert.notEqual(testAppender.events[0].data.callstack.indexOf(settings.stack[4].replace(settings.randomUserFile, settings.anonymizedRandomUserFile)), -1); + assert.equal(testAppender.events[0].data.callstack.split('\n').length, settings.stack.length); errorTelemetry.dispose(); service.dispose(); })); + test('Unexpected Error Telemetry removes PII but preserves Code file path with node modules', sinon.test(function (this: any) { + + let origErrorHandler = Errors.errorHandler.getUnexpectedErrorHandler(); + Errors.setUnexpectedErrorHandler(() => { }); + + try { + let settings = new ErrorTestingSettings(); + let testAppender = new TestTelemetryAppender(); + let service = new TelemetryService({ appender: testAppender }, undefined); + const errorTelemetry = new ErrorTelemetry(service); + + let dangerousPathWithImportantInfoError: any = new Error(settings.dangerousPathWithImportantInfo); + dangerousPathWithImportantInfoError.stack = settings.stack; + + + Errors.onUnexpectedError(dangerousPathWithImportantInfoError); + this.clock.tick(ErrorTelemetry.ERROR_FLUSH_TIMEOUT); + + assert.notEqual(testAppender.events[0].data.callstack.indexOf('(' + settings.nodeModuleAsarPathToRetain), -1); + assert.notEqual(testAppender.events[0].data.callstack.indexOf('(' + settings.nodeModulePathToRetain), -1); + assert.notEqual(testAppender.events[0].data.callstack.indexOf('(/' + settings.nodeModuleAsarPathToRetain), -1); + assert.notEqual(testAppender.events[0].data.callstack.indexOf('(/' + settings.nodeModulePathToRetain), -1); + + errorTelemetry.dispose(); + service.dispose(); + } + finally { + Errors.setUnexpectedErrorHandler(origErrorHandler); + } + })); + + test('Uncaught Error Telemetry removes PII but preserves Code file path', sinon.test(function (this: any) { + let errorStub = sinon.stub(); + window.onerror = errorStub; + let settings = new ErrorTestingSettings(); + let testAppender = new TestTelemetryAppender(); + let service = new TelemetryService({ appender: testAppender }, undefined); + const errorTelemetry = new ErrorTelemetry(service); + + let dangerousPathWithImportantInfoError: any = new Error('dangerousPathWithImportantInfo'); + dangerousPathWithImportantInfoError.stack = settings.stack; + (window.onerror)(settings.dangerousPathWithImportantInfo, 'test.js', 2, 42, dangerousPathWithImportantInfoError); + this.clock.tick(ErrorTelemetry.ERROR_FLUSH_TIMEOUT); + + assert.equal(errorStub.callCount, 1); + + assert.notEqual(testAppender.events[0].data.callstack.indexOf('(' + settings.nodeModuleAsarPathToRetain), -1); + assert.notEqual(testAppender.events[0].data.callstack.indexOf('(' + settings.nodeModulePathToRetain), -1); + assert.notEqual(testAppender.events[0].data.callstack.indexOf('(/' + settings.nodeModuleAsarPathToRetain), -1); + assert.notEqual(testAppender.events[0].data.callstack.indexOf('(/' + settings.nodeModulePathToRetain), -1); + + errorTelemetry.dispose(); + service.dispose(); + })); + + test('Unexpected Error Telemetry removes PII but preserves Code file path when PIIPath is configured', sinon.test(function (this: any) { let origErrorHandler = Errors.errorHandler.getUnexpectedErrorHandler(); @@ -455,14 +537,14 @@ suite('TelemetryService', () => { Errors.onUnexpectedError(dangerousPathWithImportantInfoError); this.clock.tick(ErrorTelemetry.ERROR_FLUSH_TIMEOUT); - assert.notEqual(testAppender.events[0].data.message.indexOf(settings.importantInfo), -1); - assert.equal(testAppender.events[0].data.message.indexOf(settings.personalInfo), -1); - assert.equal(testAppender.events[0].data.message.indexOf(settings.filePrefix), -1); - assert.notEqual(testAppender.events[0].data.stack.indexOf(settings.importantInfo), -1); - assert.equal(testAppender.events[0].data.stack.indexOf(settings.personalInfo), -1); - assert.equal(testAppender.events[0].data.stack.indexOf(settings.filePrefix), -1); - assert.notEqual(testAppender.events[0].data.stack.indexOf(settings.stack[4]), -1); - assert.equal(testAppender.events[0].data.stack.split('\n').length, settings.stack.length); + assert.notEqual(testAppender.events[0].data.msg.indexOf(settings.importantInfo), -1); + assert.equal(testAppender.events[0].data.msg.indexOf(settings.personalInfo), -1); + assert.equal(testAppender.events[0].data.msg.indexOf(settings.filePrefix), -1); + assert.notEqual(testAppender.events[0].data.callstack.indexOf(settings.importantInfo), -1); + assert.equal(testAppender.events[0].data.callstack.indexOf(settings.personalInfo), -1); + assert.equal(testAppender.events[0].data.callstack.indexOf(settings.filePrefix), -1); + assert.notEqual(testAppender.events[0].data.callstack.indexOf(settings.stack[4].replace(settings.randomUserFile, settings.anonymizedRandomUserFile)), -1); + assert.equal(testAppender.events[0].data.callstack.split('\n').length, settings.stack.length); errorTelemetry.dispose(); service.dispose(); @@ -487,14 +569,14 @@ suite('TelemetryService', () => { assert.equal(errorStub.callCount, 1); // Test that important information remains but personal info does not - assert.notEqual(testAppender.events[0].data.message.indexOf(settings.importantInfo), -1); - assert.equal(testAppender.events[0].data.message.indexOf(settings.personalInfo), -1); - assert.equal(testAppender.events[0].data.message.indexOf(settings.filePrefix), -1); - assert.notEqual(testAppender.events[0].data.stack.indexOf(settings.importantInfo), -1); - assert.equal(testAppender.events[0].data.stack.indexOf(settings.personalInfo), -1); - assert.equal(testAppender.events[0].data.stack.indexOf(settings.filePrefix), -1); - assert.notEqual(testAppender.events[0].data.stack.indexOf(settings.stack[4]), -1); - assert.equal(testAppender.events[0].data.stack.split('\n').length, settings.stack.length); + assert.notEqual(testAppender.events[0].data.msg.indexOf(settings.importantInfo), -1); + assert.equal(testAppender.events[0].data.msg.indexOf(settings.personalInfo), -1); + assert.equal(testAppender.events[0].data.msg.indexOf(settings.filePrefix), -1); + assert.notEqual(testAppender.events[0].data.callstack.indexOf(settings.importantInfo), -1); + assert.equal(testAppender.events[0].data.callstack.indexOf(settings.personalInfo), -1); + assert.equal(testAppender.events[0].data.callstack.indexOf(settings.filePrefix), -1); + assert.notEqual(testAppender.events[0].data.callstack.indexOf(settings.stack[4].replace(settings.randomUserFile, settings.anonymizedRandomUserFile)), -1); + assert.equal(testAppender.events[0].data.callstack.split('\n').length, settings.stack.length); errorTelemetry.dispose(); service.dispose(); @@ -519,14 +601,14 @@ suite('TelemetryService', () => { Errors.onUnexpectedError(missingModelError); this.clock.tick(ErrorTelemetry.ERROR_FLUSH_TIMEOUT); - assert.notEqual(testAppender.events[0].data.message.indexOf(settings.missingModelPrefix), -1); - assert.equal(testAppender.events[0].data.message.indexOf(settings.personalInfo), -1); - assert.equal(testAppender.events[0].data.message.indexOf(settings.filePrefix), -1); - assert.notEqual(testAppender.events[0].data.stack.indexOf(settings.missingModelPrefix), -1); - assert.equal(testAppender.events[0].data.stack.indexOf(settings.personalInfo), -1); - assert.equal(testAppender.events[0].data.stack.indexOf(settings.filePrefix), -1); - assert.notEqual(testAppender.events[0].data.stack.indexOf(settings.stack[4]), -1); - assert.equal(testAppender.events[0].data.stack.split('\n').length, settings.stack.length); + assert.notEqual(testAppender.events[0].data.msg.indexOf(settings.missingModelPrefix), -1); + assert.equal(testAppender.events[0].data.msg.indexOf(settings.personalInfo), -1); + assert.equal(testAppender.events[0].data.msg.indexOf(settings.filePrefix), -1); + assert.notEqual(testAppender.events[0].data.callstack.indexOf(settings.missingModelPrefix), -1); + assert.equal(testAppender.events[0].data.callstack.indexOf(settings.personalInfo), -1); + assert.equal(testAppender.events[0].data.callstack.indexOf(settings.filePrefix), -1); + assert.notEqual(testAppender.events[0].data.callstack.indexOf(settings.stack[4].replace(settings.randomUserFile, settings.anonymizedRandomUserFile)), -1); + assert.equal(testAppender.events[0].data.callstack.split('\n').length, settings.stack.length); errorTelemetry.dispose(); service.dispose(); @@ -551,14 +633,14 @@ suite('TelemetryService', () => { assert.equal(errorStub.callCount, 1); // Test that no file information remains, but this particular // error message does (Received model events for missing model) - assert.notEqual(testAppender.events[0].data.message.indexOf(settings.missingModelPrefix), -1); - assert.equal(testAppender.events[0].data.message.indexOf(settings.personalInfo), -1); - assert.equal(testAppender.events[0].data.message.indexOf(settings.filePrefix), -1); - assert.notEqual(testAppender.events[0].data.stack.indexOf(settings.missingModelPrefix), -1); - assert.equal(testAppender.events[0].data.stack.indexOf(settings.personalInfo), -1); - assert.equal(testAppender.events[0].data.stack.indexOf(settings.filePrefix), -1); - assert.notEqual(testAppender.events[0].data.stack.indexOf(settings.stack[4]), -1); - assert.equal(testAppender.events[0].data.stack.split('\n').length, settings.stack.length); + assert.notEqual(testAppender.events[0].data.msg.indexOf(settings.missingModelPrefix), -1); + assert.equal(testAppender.events[0].data.msg.indexOf(settings.personalInfo), -1); + assert.equal(testAppender.events[0].data.msg.indexOf(settings.filePrefix), -1); + assert.notEqual(testAppender.events[0].data.callstack.indexOf(settings.missingModelPrefix), -1); + assert.equal(testAppender.events[0].data.callstack.indexOf(settings.personalInfo), -1); + assert.equal(testAppender.events[0].data.callstack.indexOf(settings.filePrefix), -1); + assert.notEqual(testAppender.events[0].data.callstack.indexOf(settings.stack[4].replace(settings.randomUserFile, settings.anonymizedRandomUserFile)), -1); + assert.equal(testAppender.events[0].data.callstack.split('\n').length, settings.stack.length); errorTelemetry.dispose(); service.dispose(); @@ -583,14 +665,14 @@ suite('TelemetryService', () => { Errors.onUnexpectedError(noSuchFileError); this.clock.tick(ErrorTelemetry.ERROR_FLUSH_TIMEOUT); - assert.notEqual(testAppender.events[0].data.message.indexOf(settings.noSuchFilePrefix), -1); - assert.equal(testAppender.events[0].data.message.indexOf(settings.personalInfo), -1); - assert.equal(testAppender.events[0].data.message.indexOf(settings.filePrefix), -1); - assert.notEqual(testAppender.events[0].data.stack.indexOf(settings.noSuchFilePrefix), -1); - assert.equal(testAppender.events[0].data.stack.indexOf(settings.personalInfo), -1); - assert.equal(testAppender.events[0].data.stack.indexOf(settings.filePrefix), -1); - assert.notEqual(testAppender.events[0].data.stack.indexOf(settings.stack[4]), -1); - assert.equal(testAppender.events[0].data.stack.split('\n').length, settings.stack.length); + assert.notEqual(testAppender.events[0].data.msg.indexOf(settings.noSuchFilePrefix), -1); + assert.equal(testAppender.events[0].data.msg.indexOf(settings.personalInfo), -1); + assert.equal(testAppender.events[0].data.msg.indexOf(settings.filePrefix), -1); + assert.notEqual(testAppender.events[0].data.callstack.indexOf(settings.noSuchFilePrefix), -1); + assert.equal(testAppender.events[0].data.callstack.indexOf(settings.personalInfo), -1); + assert.equal(testAppender.events[0].data.callstack.indexOf(settings.filePrefix), -1); + assert.notEqual(testAppender.events[0].data.callstack.indexOf(settings.stack[4].replace(settings.randomUserFile, settings.anonymizedRandomUserFile)), -1); + assert.equal(testAppender.events[0].data.callstack.split('\n').length, settings.stack.length); errorTelemetry.dispose(); service.dispose(); @@ -620,14 +702,14 @@ suite('TelemetryService', () => { // Test that no file information remains, but this particular // error message does (ENOENT: no such file or directory) Errors.onUnexpectedError(noSuchFileError); - assert.notEqual(testAppender.events[0].data.message.indexOf(settings.noSuchFilePrefix), -1); - assert.equal(testAppender.events[0].data.message.indexOf(settings.personalInfo), -1); - assert.equal(testAppender.events[0].data.message.indexOf(settings.filePrefix), -1); - assert.notEqual(testAppender.events[0].data.stack.indexOf(settings.noSuchFilePrefix), -1); - assert.equal(testAppender.events[0].data.stack.indexOf(settings.personalInfo), -1); - assert.equal(testAppender.events[0].data.stack.indexOf(settings.filePrefix), -1); - assert.notEqual(testAppender.events[0].data.stack.indexOf(settings.stack[4]), -1); - assert.equal(testAppender.events[0].data.stack.split('\n').length, settings.stack.length); + assert.notEqual(testAppender.events[0].data.msg.indexOf(settings.noSuchFilePrefix), -1); + assert.equal(testAppender.events[0].data.msg.indexOf(settings.personalInfo), -1); + assert.equal(testAppender.events[0].data.msg.indexOf(settings.filePrefix), -1); + assert.notEqual(testAppender.events[0].data.callstack.indexOf(settings.noSuchFilePrefix), -1); + assert.equal(testAppender.events[0].data.callstack.indexOf(settings.personalInfo), -1); + assert.equal(testAppender.events[0].data.callstack.indexOf(settings.filePrefix), -1); + assert.notEqual(testAppender.events[0].data.callstack.indexOf(settings.stack[4].replace(settings.randomUserFile, settings.anonymizedRandomUserFile)), -1); + assert.equal(testAppender.events[0].data.callstack.split('\n').length, settings.stack.length); errorTelemetry.dispose(); service.dispose(); diff --git a/src/vs/platform/theme/common/colorRegistry.ts b/src/vs/platform/theme/common/colorRegistry.ts index e8b4f832830..dcd84b72179 100644 --- a/src/vs/platform/theme/common/colorRegistry.ts +++ b/src/vs/platform/theme/common/colorRegistry.ts @@ -86,10 +86,14 @@ class ColorRegistry implements IColorRegistry { this.colorsById = {}; } - public registerColor(id: string, defaults: ColorDefaults, description: string, needsTransparency = false): ColorIdentifier { + public registerColor(id: string, defaults: ColorDefaults, description: string, needsTransparency = false, deprecationMessage?: string): ColorIdentifier { let colorContribution = { id, description, defaults, needsTransparency }; this.colorsById[id] = colorContribution; - this.colorSchema.properties[id] = { type: 'string', description, format: 'color-hex', default: '#ff0000' }; + let propertySchema: IJSONSchema = { type: 'string', description, format: 'color-hex', default: '#ff0000' }; + if (deprecationMessage) { + propertySchema.deprecationMessage = deprecationMessage; + } + this.colorSchema.properties[id] = propertySchema; this.colorReferenceSchema.enum.push(id); this.colorReferenceSchema.enumDescriptions.push(description); return id; @@ -134,8 +138,8 @@ class ColorRegistry implements IColorRegistry { const colorRegistry = new ColorRegistry(); platform.Registry.add(Extensions.ColorContribution, colorRegistry); -export function registerColor(id: string, defaults: ColorDefaults, description: string, needsTransparency?: boolean): ColorIdentifier { - return colorRegistry.registerColor(id, defaults, description, needsTransparency); +export function registerColor(id: string, defaults: ColorDefaults, description: string, needsTransparency?: boolean, deprecationMessage?: string): ColorIdentifier { + return colorRegistry.registerColor(id, defaults, description, needsTransparency, deprecationMessage); } export function getColorRegistry(): IColorRegistry { @@ -176,7 +180,7 @@ export const inputPlaceholderForeground = registerColor('input.placeholderForegr export const inputValidationInfoBackground = registerColor('inputValidation.infoBackground', { dark: '#063B49', light: '#D6ECF2', hc: Color.black }, nls.localize('inputValidationInfoBackground', "Input validation background color for information severity.")); export const inputValidationInfoBorder = registerColor('inputValidation.infoBorder', { dark: '#007acc', light: '#007acc', hc: contrastBorder }, nls.localize('inputValidationInfoBorder', "Input validation border color for information severity.")); -export const inputValidationWarningBackground = registerColor('inputValidation.warningBackground', { dark: '#352A05', light: '#F6F5D2', hc: Color.black }, nls.localize('inputValidationWarningBackground', "Input validation background color for information warning.")); +export const inputValidationWarningBackground = registerColor('inputValidation.warningBackground', { dark: '#352A05', light: '#F6F5D2', hc: Color.black }, nls.localize('inputValidationWarningBackground', "Input validation background color for warning severity.")); export const inputValidationWarningBorder = registerColor('inputValidation.warningBorder', { dark: '#B89500', light: '#B89500', hc: contrastBorder }, nls.localize('inputValidationWarningBorder', "Input validation border color for warning severity.")); export const inputValidationErrorBackground = registerColor('inputValidation.errorBackground', { dark: '#5A1D1D', light: '#F2DEDE', hc: Color.black }, nls.localize('inputValidationErrorBackground', "Input validation background color for error severity.")); export const inputValidationErrorBorder = registerColor('inputValidation.errorBorder', { dark: '#BE1100', light: '#BE1100', hc: contrastBorder }, nls.localize('inputValidationErrorBorder', "Input validation border color for error severity.")); @@ -193,7 +197,6 @@ export const listActiveSelectionForeground = registerColor('list.activeSelection export const listInactiveSelectionBackground = registerColor('list.inactiveSelectionBackground', { dark: '#3F3F46', light: '#CCCEDB', hc: null }, nls.localize('listInactiveSelectionBackground', "List/Tree background color for the selected item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not.")); export const listInactiveSelectionForeground = registerColor('list.inactiveSelectionForeground', { dark: null, light: null, hc: null }, nls.localize('listInactiveSelectionForeground', "List/Tree foreground color for the selected item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not.")); export const listInactiveFocusBackground = registerColor('list.inactiveFocusBackground', { dark: '#313135', light: '#d8dae6', hc: null }, nls.localize('listInactiveSelectionBackground', "List/Tree background color for the selected item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not.")); -export const listInactiveFocusForeground = registerColor('list.inactiveFocusForeground', { dark: null, light: null, hc: null }, nls.localize('listInactiveSelectionForeground', "List/Tree foreground color for the selected item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not.")); export const listHoverBackground = registerColor('list.hoverBackground', { dark: '#2A2D2E', light: '#F0F0F0', hc: null }, nls.localize('listHoverBackground', "List/Tree background when hovering over items using the mouse.")); export const listHoverForeground = registerColor('list.hoverForeground', { dark: null, light: null, hc: null }, nls.localize('listHoverForeground', "List/Tree foreground when hovering over items using the mouse.")); export const listDropBackground = registerColor('list.dropBackground', { dark: listFocusBackground, light: listFocusBackground, hc: null }, nls.localize('listDropBackground', "List/Tree drag and drop background when moving items around using the mouse.")); diff --git a/src/vs/platform/theme/common/styler.ts b/src/vs/platform/theme/common/styler.ts index afb14896d04..07dfd1d9551 100644 --- a/src/vs/platform/theme/common/styler.ts +++ b/src/vs/platform/theme/common/styler.ts @@ -6,7 +6,7 @@ 'use strict'; import { ITheme, IThemeService } from 'vs/platform/theme/common/themeService'; -import { focusBorder, inputBackground, inputForeground, ColorIdentifier, selectForeground, selectBackground, selectListBackground, selectBorder, inputBorder, foreground, editorBackground, contrastBorder, inputActiveOptionBorder, listFocusBackground, listFocusForeground, listActiveSelectionBackground, listActiveSelectionForeground, listInactiveSelectionForeground, listInactiveSelectionBackground, listInactiveFocusForeground, listInactiveFocusBackground, listHoverBackground, listHoverForeground, listDropBackground, pickerGroupBorder, pickerGroupForeground, widgetShadow, inputValidationInfoBorder, inputValidationInfoBackground, inputValidationWarningBorder, inputValidationWarningBackground, inputValidationErrorBorder, inputValidationErrorBackground, activeContrastBorder, buttonForeground, buttonBackground, buttonHoverBackground, ColorFunction, lighten, badgeBackground, badgeForeground, progressBarBackground } from 'vs/platform/theme/common/colorRegistry'; +import { focusBorder, inputBackground, inputForeground, ColorIdentifier, selectForeground, selectBackground, selectListBackground, selectBorder, inputBorder, foreground, editorBackground, contrastBorder, inputActiveOptionBorder, listFocusBackground, listFocusForeground, listActiveSelectionBackground, listActiveSelectionForeground, listInactiveSelectionForeground, listInactiveSelectionBackground, listInactiveFocusBackground, listHoverBackground, listHoverForeground, listDropBackground, pickerGroupBorder, pickerGroupForeground, widgetShadow, inputValidationInfoBorder, inputValidationInfoBackground, inputValidationWarningBorder, inputValidationWarningBackground, inputValidationErrorBorder, inputValidationErrorBackground, activeContrastBorder, buttonForeground, buttonBackground, buttonHoverBackground, ColorFunction, lighten, badgeBackground, badgeForeground, progressBarBackground } from 'vs/platform/theme/common/colorRegistry'; import { IDisposable } from 'vs/base/common/lifecycle'; import { Color } from 'vs/base/common/color'; import { mixin } from 'vs/base/common/objects'; @@ -184,7 +184,6 @@ export function attachQuickOpenStyler(widget: IThemable, themeService: IThemeSer listInactiveSelectionBackground: (style && style.listInactiveSelectionBackground) || listInactiveSelectionBackground, listInactiveSelectionForeground: (style && style.listInactiveSelectionForeground) || listInactiveSelectionForeground, listInactiveFocusBackground: (style && style.listInactiveFocusBackground) || listInactiveFocusBackground, - listInactiveFocusForeground: (style && style.listInactiveFocusForeground) || listInactiveFocusForeground, listHoverBackground: (style && style.listHoverBackground) || listHoverBackground, listHoverForeground: (style && style.listHoverForeground) || listHoverForeground, listDropBackground: (style && style.listDropBackground) || listDropBackground, @@ -204,7 +203,6 @@ export interface IListStyleOverrides extends IStyleOverrides { listInactiveSelectionBackground?: ColorIdentifier; listInactiveSelectionForeground?: ColorIdentifier; listInactiveFocusBackground?: ColorIdentifier; - listInactiveFocusForeground?: ColorIdentifier; listHoverBackground?: ColorIdentifier; listHoverForeground?: ColorIdentifier; listDropBackground?: ColorIdentifier; @@ -228,7 +226,6 @@ export const defaultListStyles: IColorMapping = { listInactiveSelectionBackground: listInactiveSelectionBackground, listInactiveSelectionForeground: listInactiveSelectionForeground, listInactiveFocusBackground: listInactiveFocusBackground, - listInactiveFocusForeground: listInactiveFocusForeground, listHoverBackground: listHoverBackground, listHoverForeground: listHoverForeground, listDropBackground: listDropBackground, diff --git a/src/vs/platform/update/node/update.config.contribution.ts b/src/vs/platform/update/node/update.config.contribution.ts index 547952b04d0..755e519a36b 100644 --- a/src/vs/platform/update/node/update.config.contribution.ts +++ b/src/vs/platform/update/node/update.config.contribution.ts @@ -6,9 +6,8 @@ 'use strict'; import * as nls from 'vs/nls'; -import product from 'vs/platform/node/product'; import { Registry } from 'vs/platform/registry/common/platform'; -import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry'; +import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); configurationRegistry.registerConfiguration({ @@ -21,11 +20,13 @@ configurationRegistry.registerConfiguration({ 'type': 'string', 'enum': ['none', 'default'], 'default': 'default', + 'scope': ConfigurationScope.APPLICATION, 'description': nls.localize('updateChannel', "Configure whether you receive automatic updates from an update channel. Requires a restart after change.") }, 'update.enableWindowsBackgroundUpdates': { 'type': 'boolean', - 'default': product.quality === 'insider', + 'default': true, + 'scope': ConfigurationScope.APPLICATION, 'description': nls.localize('enableWindowsBackgroundUpdates', "Enables Windows background updates.") } } diff --git a/src/vs/platform/url/electron-browser/inactiveExtensionUrlHandler.ts b/src/vs/platform/url/electron-browser/inactiveExtensionUrlHandler.ts new file mode 100644 index 00000000000..9f9c05762c9 --- /dev/null +++ b/src/vs/platform/url/electron-browser/inactiveExtensionUrlHandler.ts @@ -0,0 +1,122 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IURLService, IURLHandler } from 'vs/platform/url/common/url'; +import URI from 'vs/base/common/uri'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { IDisposable, toDisposable, combinedDisposable } from 'vs/base/common/lifecycle'; + +const FIVE_MINUTES = 5 * 60 * 1000; +const THIRTY_SECONDS = 30 * 1000; + +function isExtensionId(value: string): boolean { + return /^[a-z0-9][a-z0-9\-]*\.[a-z0-9][a-z0-9\-]*$/i.test(value); +} + +export const IInactiveExtensionUrlHandler = createDecorator('inactiveExtensionUrlHandler'); + +export interface IInactiveExtensionUrlHandler { + readonly _serviceBrand: any; + registerExtensionHandler(extensionId: string, handler: IURLHandler): void; + unregisterExtensionHandler(extensionId: string): void; +} + +/** + * This class handles URLs which are directed towards inactive extensions. + * If a URL is directed towards an inactive extension, it buffers it, + * activates the extension and re-opens the URL once the extension registers + * a URL handler. If the extension never registers a URL handler, the urls + * will eventually be garbage collected. + */ +export class InactiveExtensionUrlHandler implements IInactiveExtensionUrlHandler, IURLHandler { + + readonly _serviceBrand: any; + + private extensionIds = new Set(); + private uriBuffer = new Map(); + private disposable: IDisposable; + + constructor( + @IURLService urlService: IURLService, + @IExtensionService private extensionService: IExtensionService + ) { + const interval = setInterval(() => this.garbageCollect(), THIRTY_SECONDS); + + this.disposable = combinedDisposable([ + urlService.registerHandler(this), + toDisposable(() => clearInterval(interval)) + ]); + } + + handleURL(uri: URI): TPromise { + if (!isExtensionId(uri.authority)) { + return TPromise.as(false); + } + + const extensionId = uri.authority; + + // let the ExtensionUrlHandler instance handle this + if (this.extensionIds.has(extensionId)) { + return TPromise.as(false); + } + + // collect URI for eventual extension activation + const timestamp = new Date().getTime(); + let uris = this.uriBuffer.get(extensionId); + + if (!uris) { + uris = []; + this.uriBuffer.set(extensionId, uris); + } + + uris.push({ timestamp, uri }); + + // activate the extension + return this.extensionService.activateByEvent(`onExternalUri:${extensionId}`) + .then(() => true); + } + + registerExtensionHandler(extensionId: string, handler: IURLHandler): void { + this.extensionIds.add(extensionId); + + const uris = this.uriBuffer.get(extensionId) || []; + + for (const { uri } of uris) { + handler.handleURL(uri); + } + + this.uriBuffer.delete(extensionId); + } + + unregisterExtensionHandler(extensionId: string): void { + this.extensionIds.delete(extensionId); + } + + // forget about all uris buffered more than 5 minutes ago + private garbageCollect(): void { + console.log('garbage collect'); + + const now = new Date().getTime(); + const uriBuffer = new Map(); + + this.uriBuffer.forEach((uris, extensionId) => { + uris = uris.filter(({ timestamp }) => now - timestamp < FIVE_MINUTES); + + if (uris.length > 0) { + uriBuffer.set(extensionId, uris); + } + }); + + this.uriBuffer = uriBuffer; + } + + dispose(): void { + this.disposable.dispose(); + this.extensionIds.clear(); + this.uriBuffer.clear(); + } +} \ No newline at end of file diff --git a/src/vs/platform/url/electron-main/electronUrlListener.ts b/src/vs/platform/url/electron-main/electronUrlListener.ts index 418e08c5b91..d228fdc23ef 100644 --- a/src/vs/platform/url/electron-main/electronUrlListener.ts +++ b/src/vs/platform/url/electron-main/electronUrlListener.ts @@ -32,7 +32,7 @@ export class ElectronURLListener { @IURLService private urlService: IURLService, @IWindowsMainService private windowsService: IWindowsMainService ) { - const globalBuffer = (global.getOpenUrls() || []) as string[]; + const globalBuffer = ((global).getOpenUrls() || []) as string[]; const rawBuffer = [ ...(typeof initial === 'string' ? [initial] : initial), ...globalBuffer diff --git a/src/vs/platform/windows/common/windows.ts b/src/vs/platform/windows/common/windows.ts index 5c04d8c0640..5df0fccaa20 100644 --- a/src/vs/platform/windows/common/windows.ts +++ b/src/vs/platform/windows/common/windows.ts @@ -226,6 +226,7 @@ export interface IWindowSettings { enableMenuBarMnemonics: boolean; closeWhenEmpty: boolean; smoothScrollingWorkaround: boolean; + clickThroughInactive: boolean; } export enum OpenContext { diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index a890f927ccf..93650ca4d4c 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -537,6 +537,20 @@ declare module 'vscode' { kind?: TextEditorSelectionChangeKind; } + /** + * Represents an event describing the change in a [text editor's visible ranges](#TextEditor.visibleRanges). + */ + export interface TextEditorVisibleRangesChangeEvent { + /** + * The [text editor](#TextEditor) for which the visible ranges have changed. + */ + textEditor: TextEditor; + /** + * The new value for the [text editor's visible ranges](#TextEditor.visibleRanges). + */ + visibleRanges: Range[]; + } + /** * Represents an event describing the change in a [text editor's options](#TextEditor.options). */ @@ -879,6 +893,11 @@ declare module 'vscode' { */ color?: string | ThemeColor; + /** + * CSS styling property that will be applied to text enclosed by a decoration. + */ + opacity?: string; + /** * CSS styling property that will be applied to text enclosed by a decoration. */ @@ -1062,6 +1081,12 @@ declare module 'vscode' { */ selections: Selection[]; + /** + * The current visible ranges in the editor (vertically). + * This accounts only for vertical scrolling, and not for horizontal scrolling. + */ + readonly visibleRanges: Range[]; + /** * Text editor options. */ @@ -1502,12 +1527,20 @@ declare module 'vscode' { /** * A human readable string which is rendered less prominent. */ - description: string; + description?: string; /** * A human readable string which is rendered less prominent. */ detail?: string; + + /** + * Optional flag indicating if this item is picked initially. + * (Only honored when the picker allows multiple selections.) + * + * @see [QuickPickOptions.canPickMany](#QuickPickOptions.canPickMany) + */ + picked?: boolean; } /** @@ -1534,6 +1567,11 @@ declare module 'vscode' { */ ignoreFocusOut?: boolean; + /** + * An optional flag to make the picker accept multiple selections, if true the result is an array of picks. + */ + canPickMany?: boolean; + /** * An optional function that is invoked whenever an item is selected. */ @@ -1771,7 +1809,7 @@ declare module 'vscode' { * its resource, or a glob-pattern that is applied to the [path](#TextDocument.fileName). * * @sample A language filter that applies to typescript files on disk: `{ language: 'typescript', scheme: 'file' }` - * @sample A language filter that applies to all package.json paths: `{ language: 'json', pattern: '**​/package.json' }` + * @sample A language filter that applies to all package.json paths: `{ language: 'json', scheme: 'untitled', pattern: '**​/package.json' }` */ export interface DocumentFilter { @@ -1796,10 +1834,14 @@ declare module 'vscode' { * A language selector is the combination of one or many language identifiers * and [language filters](#DocumentFilter). * - * @sample `let sel:DocumentSelector = 'typescript'`; - * @sample `let sel:DocumentSelector = ['typescript', { language: 'json', pattern: '**​/tsconfig.json' }]`; + * *Note* that a document selector that is just a language identifier selects *all* + * documents, even those that are not saved on disk. Only use such selectors when + * a feature works without further context, e.g without the need to resolve related + * 'files'. + * + * @sample `let sel:DocumentSelector = { scheme: 'file', language: 'typescript' }`; */ - export type DocumentSelector = string | DocumentFilter | (string | DocumentFilter)[]; + export type DocumentSelector = DocumentFilter | string | Array; /** * A provider result represents the values a provider, like the [`HoverProvider`](#HoverProvider), @@ -1845,17 +1887,17 @@ declare module 'vscode' { static readonly Empty: CodeActionKind; /** - * Base kind for quickfix actions. + * Base kind for quickfix actions: `quickfix` */ static readonly QuickFix: CodeActionKind; /** - * Base kind for refactoring actions. + * Base kind for refactoring actions: `refactor` */ static readonly Refactor: CodeActionKind; /** - * Base kind for refactoring extraction actions. + * Base kind for refactoring extraction actions: `refactor.extract` * * Example extract actions: * @@ -1868,7 +1910,7 @@ declare module 'vscode' { static readonly RefactorExtract: CodeActionKind; /** - * Base kind for refactoring inline actions. + * Base kind for refactoring inline actions: `refactor.inline` * * Example inline actions: * @@ -1880,7 +1922,7 @@ declare module 'vscode' { static readonly RefactorInline: CodeActionKind; /** - * Base kind for refactoring rewrite actions. + * Base kind for refactoring rewrite actions: `refactor.rewrite` * * Example rewrite actions: * @@ -1893,6 +1935,18 @@ declare module 'vscode' { */ static readonly RefactorRewrite: CodeActionKind; + /** + * Base kind for source actions: `source` + * + * Source code actions apply to the entire file. + */ + static readonly Source: CodeActionKind; + + /** + * Base kind for an organize imports source action: `source.organizeImports` + */ + static readonly SourceOrganizeImports: CodeActionKind; + private constructor(value: string); /** @@ -1989,7 +2043,6 @@ declare module 'vscode' { * A code action can be any command that is [known](#commands.getCommands) to the system. */ export interface CodeActionProvider { - /** * Provide commands for the given document and range. * @@ -2003,6 +2056,19 @@ declare module 'vscode' { provideCodeActions(document: TextDocument, range: Range, context: CodeActionContext, token: CancellationToken): ProviderResult<(Command | CodeAction)[]>; } + /** + * Metadata about the type of code actions that a [CodeActionProvider](#CodeActionProvider) providers + */ + export interface CodeActionProviderMetadata { + /** + * [CodeActionKinds](#CodeActionKind) that this provider may return. + * + * The list of kinds may be generic, such as `CodeActionKind.Refactor`, or the provider + * may list our every specific kind they provide, such as `CodeActionKind.Refactor.Extract.append('function`)` + */ + readonly providedCodeActionKinds?: ReadonlyArray; + } + /** * A code lens represents a [command](#Command) that should be shown along with * source text, like the number of references, a way to run tests, etc. @@ -2685,6 +2751,18 @@ declare module 'vscode' { * signaled by returning `undefined` or `null`. */ provideRenameEdits(document: TextDocument, position: Position, newName: string, token: CancellationToken): ProviderResult; + + /** + * Optional function for resolving and validating a position *before* running rename. The result can + * be a range or a range and a placeholder text. The placeholder text should be the identifier of the symbol + * which is being renamed - when omitted the text in the returned range is used. + * + * @param document The document in which rename will be invoked. + * @param position The position at which rename will be invoked. + * @param token A cancellation token. + * @return The range or range and placeholder text of the identifier that is to be renamed. The lack of a result can signaled by returning `undefined` or `null`. + */ + resolveRenameLocation?(document: TextDocument, position: Position, token: CancellationToken): ProviderResult; } /** @@ -3311,6 +3389,90 @@ declare module 'vscode' { provideColorPresentations(color: Color, context: { document: TextDocument, range: Range }, token: CancellationToken): ProviderResult; } + export class FoldingRange { + + /** + * The zero-based start line of the range to fold. The folded area starts after the line's last character. + */ + start: number; + + /** + * The zero-based end line of the range to fold. The folded area ends with the line's last character. + */ + end: number; + + /** + * Describes the [Kind](#FoldingRangeKind) of the folding range such as [Comment](#FoldingRangeKind.Comment) or + * [Region](#FoldingRangeKind.Region). The kind is used to categorize folding ranges and used by commands + * like 'Fold all comments'. See + * [FoldingRangeKind](#FoldingRangeKind) for an enumeration of standardized kinds. + */ + kind?: FoldingRangeKind; + + /** + * Creates a new folding range. + * + * @param start The start line of the folded range. + * @param end The end line of the folded range. + * @param kind The kind of the folding range. + */ + constructor(start: number, end: number, kind?: FoldingRangeKind); + } + + export class FoldingRangeKind { + /** + * Kind for folding range representing a comment. The value of the kind is 'comment'. + */ + static readonly Comment: FoldingRangeKind; + /** + * Kind for folding range representing a import. The value of the kind is 'imports'. + */ + static readonly Imports: FoldingRangeKind; + /** + * Kind for folding range representing regions (for example a folding range marked by `#region` and `#endregion`). + * The value of the kind is 'region'. + */ + static readonly Region: FoldingRangeKind; + /** + * String value of the kind, e.g. `comment`. + */ + readonly value: string; + /** + * Creates a new [FoldingRangeKind](#FoldingRangeKind). + * + * @param value of the kind. + */ + public constructor(value: string); + } + + + /** + * Metadata about the kind of folding ranges that a [FoldingRangeProvider](#FoldingRangeProvider) providers uses. + */ + export interface FoldingRangeProviderMetadata { + /** + * [FoldingRangeKind](#FoldingRangeKind) that this provider may return. + */ + readonly providedFoldingRangeKinds?: ReadonlyArray; + } + + /** + * Folding context (for future use) + */ + export interface FoldingContext { + } + + export interface FoldingRangeProvider { + /** + * Returns a list of folding ranges or null and undefined if the provider + * does not want to participate or was cancelled. + * @param document The document in which the command was invoked. + * @param context Additional context information (for future use) + * @param token A cancellation token. + */ + provideFoldingRanges(document: TextDocument, context: FoldingContext, token: CancellationToken): ProviderResult; + } + /** * A tuple of two characters, like a pair of * opening and closing brackets. @@ -3647,6 +3809,17 @@ declare module 'vscode' { constructor(uri: Uri, rangeOrPosition: Range | Position); } + /** + * The event that is fired when diagnostics change. + */ + export interface DiagnosticChangeEvent { + + /** + * An array of resources for which diagnostics have changed. + */ + readonly uris: Uri[]; + } + /** * Represents the severity of diagnostics. */ @@ -3716,29 +3889,29 @@ declare module 'vscode' { */ message: string; - /** - * A human-readable string describing the source of this - * diagnostic, e.g. 'typescript' or 'super lint'. - */ - source: string; - /** * The severity, default is [error](#DiagnosticSeverity.Error). */ severity: DiagnosticSeverity; + /** + * A human-readable string describing the source of this + * diagnostic, e.g. 'typescript' or 'super lint'. + */ + source?: string; + /** * A code or identifier for this diagnostics. Will not be surfaced * to the user, but should be used for later processing, e.g. when * providing [code actions](#CodeActionContext). */ - code: string | number; + code?: string | number; /** * An array of related diagnostic information, e.g. when symbol-names within * a scope collide all definitions can be marked via this property. */ - relatedInformation: DiagnosticRelatedInformation[]; + relatedInformation?: DiagnosticRelatedInformation[]; /** * Creates a new diagnostic object. @@ -4005,7 +4178,8 @@ declare module 'vscode' { /** * Report a progress update. - * @param value A progress item, like a message or an updated percentage value + * @param value A progress item, like a message and/or an + * report on how much work finished */ report(value: T): void; } @@ -4646,6 +4820,175 @@ declare module 'vscode' { resolveTask(task: Task, token?: CancellationToken): ProviderResult; } + /** + * Content settings for a webview. + */ + export interface WebviewOptions { + /** + * Should scripts be enabled in the webview content? + * + * Defaults to false (scripts-disabled). + */ + readonly enableScripts?: boolean; + + /** + * Should command uris be enabled in webview content? + * + * Defaults to false. + */ + readonly enableCommandUris?: boolean; + + /** + * Root paths from which the webview can load local (filesystem) resources using the `vscode-resource:` scheme. + * + * Default to the root folders of the current workspace plus the extension's install directory. + * + * Pass in an empty array to disallow access to any local resources. + */ + readonly localResourceRoots?: ReadonlyArray; + } + + /** + * A webview displays html content, like an iframe. + */ + export interface Webview { + /** + * Content settings for the webview. + */ + readonly options: WebviewOptions; + + /** + * Contents of the webview. + * + * Should be a complete html document. + */ + html: string; + + /** + * Fired when the webview content posts a message. + */ + readonly onDidReceiveMessage: Event; + + /** + * Post a message to the webview content. + * + * Messages are only develivered if the webview is visible. + * + * @param message Body of the message. + */ + postMessage(message: any): Thenable; + } + + /** + * Content settings for a webview panel. + */ + export interface WebviewPanelOptions { + /** + * Should the find widget be enabled in the panel? + * + * Defaults to false. + */ + readonly enableFindWidget?: boolean; + + /** + * Should the webview panel's content (iframe) be kept around even when the panel + * is no longer visible? + * + * Normally the webview panel's html context is created when the panel becomes visible + * and destroyed when it is is hidden. Extensions that have complex state + * or UI can set the `retainContextWhenHidden` to make VS Code keep the webview + * context around, even when the webview moves to a background tab. When + * the panel becomes visible again, the context is automatically restored + * in the exact same state it was in originally. + * + * `retainContextWhenHidden` has a high memory overhead and should only be used if + * your panel's context cannot be quickly saved and restored. + */ + readonly retainContextWhenHidden?: boolean; + } + + /** + * A panel that contains a webview. + */ + interface WebviewPanel { + /** + * Type of the webview panel, such as `'markdown.preview'`. + */ + readonly viewType: string; + + /** + * Title of the panel shown in UI. + */ + title: string; + + /** + * Webview belonging to the panel. + */ + readonly webview: Webview; + + /** + * Content settings for the webview panel. + */ + readonly options: WebviewPanelOptions; + + /** + * Editor position of the panel. This property is only set if the webview is in + * one of the three editor view columns. + * + * @deprecated + */ + readonly viewColumn?: ViewColumn; + + /** + * Is the panel currently visible? + */ + readonly visible: boolean; + + /** + * Fired when the panel's view state changes. + */ + readonly onDidChangeViewState: Event; + + /** + * Fired when the panel is disposed. + * + * This may be because the user closed the panel or because `.dispose()` was + * called on it. + * + * Trying to use the panel after it has been disposed throws an exception. + */ + readonly onDidDispose: Event; + + /** + * Show the webview panel in a given column. + * + * A webview panel may only show in a single column at a time. If it is already showing, this + * method moves it to a new column. + * + * @param viewColumn View column to show the panel in. Shows in the current `viewColumn` if undefined. + */ + reveal(viewColumn?: ViewColumn): void; + + /** + * Dispose of the webview panel. + * + * This closes the panel if it showing and disposes of the resources owned by the webview. + * Webview panels are also disposed when the user closes the webview panel. Both cases + * fire the `onDispose` event. + */ + dispose(): any; + } + + /** + * Event fired when a webview panel's view state changes. + */ + export interface WebviewPanelOnDidChangeViewStateEvent { + /** + * Webview panel whose view state changed. + */ + readonly webviewPanel: WebviewPanel; + } + /** * Namespace describing the environment the editor runs in. */ @@ -4830,6 +5173,11 @@ declare module 'vscode' { */ export const onDidChangeTextEditorSelection: Event; + /** + * An [event](#Event) which fires when the selection in an editor has changed. + */ + export const onDidChangeTextEditorVisibleRanges: Event; + /** * An [event](#Event) which fires when the options of an editor have changed. */ @@ -5036,6 +5384,16 @@ declare module 'vscode' { */ export function showErrorMessage(message: string, options: MessageOptions, ...items: T[]): Thenable; + /** + * Shows a selection list allowing multiple selections. + * + * @param items An array of strings, or a promise that resolves to an array of strings. + * @param options Configures the behavior of the selection list. + * @param token A token that can be used to signal cancellation. + * @return A promise that resolves to the selected items or `undefined`. + */ + export function showQuickPick(items: string[] | Thenable, options: QuickPickOptions & { canPickMany: true; }, token?: CancellationToken): Thenable; + /** * Shows a selection list. * @@ -5046,6 +5404,16 @@ declare module 'vscode' { */ export function showQuickPick(items: string[] | Thenable, options?: QuickPickOptions, token?: CancellationToken): Thenable; + /** + * Shows a selection list allowing multiple selections. + * + * @param items An array of items, or a promise that resolves to an array of items. + * @param options Configures the behavior of the selection list. + * @param token A token that can be used to signal cancellation. + * @return A promise that resolves to the selected items or `undefined`. + */ + export function showQuickPick(items: T[] | Thenable, options: QuickPickOptions & { canPickMany: true; }, token?: CancellationToken): Thenable; + /** * Shows a selection list. * @@ -5103,6 +5471,18 @@ declare module 'vscode' { */ export function createOutputChannel(name: string): OutputChannel; + /** + * Create and show a new webview panel. + * + * @param viewType Identifies the type of the webview panel. + * @param title Title of the panel. + * @param position Editor column to show the new panel in. + * @param options Settings for the new panel. + * + * @return New webview panel. + */ + export function createWebviewPanel(viewType: string, title: string, position: ViewColumn, options: WebviewPanelOptions & WebviewOptions): WebviewPanel; + /** * Set a message to the status bar. This is a short hand for the more powerful * status bar [items](#window.createStatusBarItem). @@ -5155,9 +5535,10 @@ declare module 'vscode' { * @param task A callback returning a promise. Progress state can be reported with * the provided [progress](#Progress)-object. * - * To report discrete progress, use `percentage` to indicate how much work has been completed. Each call with - * a `percentage` value will be summed up and reflected as overall progress until 100% is reached. Note that - * currently only `ProgressLocation.Notification` is capable of showing discrete progress. + * To report discrete progress, use `increment` to indicate how much work has been completed. Each call with + * a `increment` value will be summed up and reflected as overall progress until 100% is reached (a value of + * e.g. `10` accounts for `10%` of work done). + * Note that currently only `ProgressLocation.Notification` is capable of showing discrete progress. * * To monitor if the operation has been cancelled by the user, use the provided [`CancellationToken`](#CancellationToken). * Note that currently only `ProgressLocation.Notification` is supporting to show a cancel button to cancel the @@ -5165,7 +5546,7 @@ declare module 'vscode' { * * @return The thenable the task-callback returned. */ - export function withProgress(options: ProgressOptions, task: (progress: Progress<{ message?: string; percentage?: number }>, token: CancellationToken) => Thenable): Thenable; + export function withProgress(options: ProgressOptions, task: (progress: Progress<{ message?: string; increment?: number }>, token: CancellationToken) => Thenable): Thenable; /** * Creates a status bar [item](#StatusBarItem). @@ -5199,7 +5580,8 @@ declare module 'vscode' { /** * Register a [TreeDataProvider](#TreeDataProvider) for the view contributed using the extension point `views`. * This will allow you to contribute data to the [TreeView](#TreeView) and update if the data changes. - * To get access to the [TreeView](#TreeView) and perform operations on it, use [createTreeView](#window.createTreeView). + * + * **Note:** To get access to the [TreeView](#TreeView) and perform operations on it, use [createTreeView](#window.createTreeView). * * @param viewId Id of the view contributed using the extension point `views`. * @param treeDataProvider A [TreeDataProvider](#TreeDataProvider) that provides tree data for the view @@ -5450,6 +5832,10 @@ declare module 'vscode' { * The range that got replaced. */ range: Range; + /** + * The offset of the range that got replaced. + */ + rangeOffset: number; /** * The length of the range that got replaced. */ @@ -5979,6 +6365,29 @@ declare module 'vscode' { */ export function match(selector: DocumentSelector, document: TextDocument): number; + /** + * An [event](#Event) which fires when the global set of diagnostics changes. This is + * newly added and removed diagnostics. + */ + export const onDidChangeDiagnostics: Event; + + /** + * Get all diagnostics for a given resource. *Note* that this includes diagnostics from + * all extensions but *not yet* from the task framework. + * + * @param resource A resource + * @returns An arrary of [diagnostics](#Diagnostic) objects or an empty array. + */ + export function getDiagnostics(resource: Uri): Diagnostic[]; + + /** + * Get all diagnostics. *Note* that this includes diagnostics from + * all extensions but *not yet* from the task framework. + * + * @returns An array of uri-diagnostics tuples or an empty array. + */ + export function getDiagnostics(): [Uri, Diagnostic[]][]; + /** * Create a diagnostics collection. * @@ -6012,9 +6421,10 @@ declare module 'vscode' { * * @param selector A selector that defines the documents this provider is applicable to. * @param provider A code action provider. + * @param metadata Metadata about the kind of code actions the provider providers. * @return A [disposable](#Disposable) that unregisters this provider when being disposed. */ - export function registerCodeActionsProvider(selector: DocumentSelector, provider: CodeActionProvider): Disposable; + export function registerCodeActionsProvider(selector: DocumentSelector, provider: CodeActionProvider, metadata?: CodeActionProviderMetadata): Disposable; /** * Register a code lens provider. @@ -6230,6 +6640,20 @@ declare module 'vscode' { */ export function registerColorProvider(selector: DocumentSelector, provider: DocumentColorProvider): Disposable; + /** + * Register a folding range provider. + * + * Multiple folding can be registered for a language. In that case providers are sorted + * by their [score](#languages.match) and the best-matching provider is used. Failure + * of the selected provider will cause a failure of the whole operation. + * + * @param selector A selector that defines the documents this provider is applicable to. + * @param provider A folding range provider. + * @param metadata Metadata about the kind of code actions the provider providers. + * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + */ + export function registerFoldingRangeProvider(selector: DocumentSelector, provider: FoldingRangeProvider, metadata?: FoldingRangeProviderMetadata): Disposable; + /** * Set a [language configuration](#LanguageConfiguration) for a language. * diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 28b6923f3fa..8c7906ceae9 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -11,172 +11,7 @@ declare module 'vscode' { export function sampleFunction(): Thenable; } - //#region Joh: readable diagnostics - - export interface DiagnosticChangeEvent { - uris: Uri[]; - } - - export namespace languages { - - /** - * - */ - export const onDidChangeDiagnostics: Event; - - /** - * - */ - export function getDiagnostics(resource: Uri): Diagnostic[]; - - /** - * - */ - export function getDiagnostics(): [Uri, Diagnostic[]][]; - } - - //#endregion - - //#region Aeschli: folding - - export class FoldingRangeList { - - /** - * The folding ranges. - */ - ranges: FoldingRange[]; - - /** - * Creates mew folding range list. - * - * @param ranges The folding ranges - */ - constructor(ranges: FoldingRange[]); - } - - - export class FoldingRange { - - /** - * The start line number (zero-based) of the range to fold. The hidden area starts after the last character of that line. - */ - startLine: number; - - /** - * The end line number (0-based) of the range to fold. The hidden area ends at the last character of that line. - */ - endLine: number; - - /** - * The actual color value for this color range. - */ - type?: FoldingRangeType | string; - - /** - * Creates a new folding range. - * - * @param startLineNumber The first line of the fold - * @param type The last line of the fold - */ - constructor(startLineNumber: number, endLineNumber: number, type?: FoldingRangeType | string); - } - - export enum FoldingRangeType { - /** - * Folding range for a comment - */ - Comment = 'comment', - /** - * Folding range for a imports or includes - */ - Imports = 'imports', - /** - * Folding range for a region (e.g. `#region`) - */ - Region = 'region' - } - - export namespace languages { - - /** - * Register a folding provider. - * - * Multiple folding can be registered for a language. In that case providers are sorted - * by their [score](#languages.match) and the best-matching provider is used. Failure - * of the selected provider will cause a failure of the whole operation. - * - * @param selector A selector that defines the documents this provider is applicable to. - * @param provider A folding provider. - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. - */ - export function registerFoldingProvider(selector: DocumentSelector, provider: FoldingProvider): Disposable; - } - - export interface FoldingContext { - maxRanges?: number; - } - - export interface FoldingProvider { - /** - * Returns a list of folding ranges or null if the provider does not want to participate or was cancelled. - */ - provideFoldingRanges(document: TextDocument, context: FoldingContext, token: CancellationToken): ProviderResult; - } - - //#endregion - - //#region Joh: file system provider - - // export enum FileErrorCodes { - // /** - // * Not owner. - // */ - // EPERM = 1, - // /** - // * No such file or directory. - // */ - // ENOENT = 2, - // /** - // * I/O error. - // */ - // EIO = 5, - // /** - // * Permission denied. - // */ - // EACCES = 13, - // /** - // * File exists. - // */ - // EEXIST = 17, - // /** - // * Not a directory. - // */ - // ENOTDIR = 20, - // /** - // * Is a directory. - // */ - // EISDIR = 21, - // /** - // * File too large. - // */ - // EFBIG = 27, - // /** - // * No space left on device. - // */ - // ENOSPC = 28, - // /** - // * Directory is not empty. - // */ - // ENOTEMPTY = 66, - // /** - // * Invalid file handle. - // */ - // ESTALE = 70, - // /** - // * Illegal NFS file handle. - // */ - // EBADHANDLE = 10001, - // } + //#region Joh: file system provider (OLD) export enum FileChangeType { Updated = 0, @@ -211,7 +46,7 @@ declare module 'vscode' { readonly onDidChange?: Event; // more... - // + // @deprecated - will go away utimes(resource: Uri, mtime: number, atime: number): Thenable; stat(resource: Uri): Thenable; @@ -249,8 +84,177 @@ declare module 'vscode' { // create(resource: Uri): Thenable; } + export type DeprecatedFileChangeType = FileChangeType; + export type DeprecatedFileType = FileType; + export type DeprecatedFileChange = FileChange; + export type DeprecatedFileStat = FileStat; + export type DeprecatedFileSystemProvider = FileSystemProvider; + export namespace workspace { - export function registerFileSystemProvider(scheme: string, provider: FileSystemProvider): Disposable; + export function registerDeprecatedFileSystemProvider(scheme: string, provider: DeprecatedFileSystemProvider): Disposable; + export function registerFileSystemProvider(scheme: string, provider: FileSystemProvider, newProvider?: FileSystemProvider2): Disposable; + } + + //#endregion + + //#region Joh: file system provider (new) + + /** + * + */ + export class FileSystemError extends Error { + + static EntryExists(message?: string): FileSystemError; + static EntryNotFound(message?: string): FileSystemError; + static EntryNotADirectory(message?: string): FileSystemError; + static EntryIsADirectory(message?: string): FileSystemError; + + constructor(message?: string); + } + + export enum FileChangeType2 { + Changed = 1, + Created = 2, + Deleted = 3, + } + + export interface FileChange2 { + type: FileChangeType2; + uri: Uri; + } + + export interface FileStat2 { + isFile: boolean; + isDirectory: boolean; + isSymbolicLink: boolean; + mtime: number; + size: number; + } + + /** + * + */ + export interface FileOptions { + + /** + * Create a file when it doesn't exists + */ + create?: boolean; + + /** + * In combination with [`create`](FileOptions.create) but + * the operation should fail when a file already exists. + */ + exclusive?: boolean; + + /** + * Open a file for reading. + */ + read?: boolean; + + /** + * Open a file for writing. + */ + write?: boolean; + } + + /** + * + */ + export interface FileSystemProvider2 { + + _version: 9; + + /** + * An event to signal that a resource has been created, changed, or deleted. This + * event should fire for resources that are being [watched](#FileSystemProvider2.watch) + * by clients of this provider. + */ + readonly onDidChangeFile: Event; + + /** + * Subscribe to events in the file or folder denoted by `uri`. + * @param uri + * @param options + */ + watch(uri: Uri, options: { recursive?: boolean; excludes?: string[] }): Disposable; + + /** + * Retrieve metadata about a file. Throw an [`EntryNotFound`](#FileError.EntryNotFound)-error + * in case the file does not exist. + * + * @param uri The uri of the file to retrieve meta data about. + * @param token A cancellation token. + * @return The file metadata about the file. + */ + stat(uri: Uri, options: { /*future: followSymlinks*/ }, token: CancellationToken): FileStat2 | Thenable; + + /** + * Retrieve the meta data of all entries of a [directory](#FileType2.Directory) + * + * @param uri The uri of the folder. + * @param token A cancellation token. + * @return A thenable that resolves to an array of tuples of file names and files stats. + */ + readDirectory(uri: Uri, options: { /*future: onlyType?*/ }, token: CancellationToken): [string, FileStat2][] | Thenable<[string, FileStat2][]>; + + /** + * Create a new directory. *Note* that new files are created via `write`-calls. + * + * @param uri The uri of the *new* folder. + * @param token A cancellation token. + */ + createDirectory(uri: Uri, options: { /*future: permissions?*/ }, token: CancellationToken): FileStat2 | Thenable; + + /** + * Read the entire contents of a file. + * + * @param uri The uri of the file. + * @param token A cancellation token. + * @return A thenable that resolves to an array of bytes. + */ + readFile(uri: Uri, options: FileOptions, token: CancellationToken): Uint8Array | Thenable; + + /** + * Write data to a file, replacing its entire contents. + * + * @param uri The uri of the file. + * @param content The new content of the file. + * @param token A cancellation token. + */ + writeFile(uri: Uri, content: Uint8Array, options: FileOptions, token: CancellationToken): void | Thenable; + + /** + * Delete a file or folder from the underlying storage. + * + * @param uri The resource that is to be deleted + * @param options Options bag for future use + * @param token A cancellation token. + */ + delete(uri: Uri, options: { /*future: useTrash?, followSymlinks?*/ }, token: CancellationToken): void | Thenable; + + /** + * Rename a file or folder. + * + * @param oldUri The existing file or folder. + * @param newUri The target location. + * @param token A cancellation token. + */ + rename(oldUri: Uri, newUri: Uri, options: FileOptions, token: CancellationToken): FileStat2 | Thenable; + + /** + * Copy files or folders. Implementing this function is optional but it will speedup + * the copy operation. + * + * @param uri The existing file or folder. + * @param target The target location. + * @param token A cancellation token. + */ + copy?(uri: Uri, target: Uri, options: FileOptions, token: CancellationToken): FileStat2 | Thenable; + } + + export namespace workspace { + export function registerFileSystemProvider2(scheme: string, provider: FileSystemProvider2, options: { isCaseSensitive?: boolean }): Disposable; } //#endregion @@ -437,31 +441,6 @@ declare module 'vscode' { //#endregion - //#region Joh: rename context - - export interface RenameContext { - range?: Range; - newName?: string; - message?: string; - } - - export interface RenameProvider2 extends RenameProvider { - - /** - * Optional function for resolving and validating a position at which rename is - * being carried out. - * - * @param document The document in which rename will be invoked. - * @param position The position at which rename will be invoked. - * @param token A cancellation token. - * @return A `RenameContext` with more information. The lack of a result can signaled by returning `undefined` or `null`. - */ - resolveRenameLocation?(document: TextDocument, position: Position, token: CancellationToken): ProviderResult; - - } - - //#endregion - //#region Joao: SCM validation /** @@ -512,179 +491,50 @@ declare module 'vscode' { //#endregion - //#region Matt: WebView + //#region Matt: WebView Serializer /** - * Content settings for a webview. + * Save and restore webview panels that have been persisted when vscode shuts down. */ - export interface WebviewOptions { + interface WebviewPanelSerializer { /** - * Should scripts be enabled in the webview content? + * Save a webview panel's `state`. * - * Defaults to false (scripts-disabled). - */ - readonly enableScripts?: boolean; - - /** - * Should command uris be enabled in webview content? + * Called before shutdown. Extensions have a 250ms timeframe to return a state. If serialization + * takes longer than 250ms, the panel will not be serialized. * - * Defaults to false. - */ - readonly enableCommandUris?: boolean; - - /** - * Should the find widget be enabled in the webview? + * @param webviewPanel webview Panel to serialize. May or may not be visible. * - * Defaults to false. + * @returns JSON serializable state blob. */ - readonly enableFindWidget?: boolean; + serializeWebviewPanel(webviewPanel: WebviewPanel): Thenable; /** - * Should the webview's context be kept around even when the webview is no longer visible? + * Restore a webview panel from its seriailzed `state`. * - * Normally a webview's context is created when the webview becomes visible - * and destroyed when the webview is hidden. Apps that have complex state - * or UI can set the `retainContextWhenHidden` to make VS Code keep the webview - * context around, even when the webview moves to a background tab. When - * the webview becomes visible again, the context is automatically restored - * in the exact same state it was in originally. + * Called when a serialized webview first becomes visible. * - * `retainContextWhenHidden` has a high memory overhead and should only be used if - * your webview's context cannot be quickly saved and restored. - */ - readonly retainContextWhenHidden?: boolean; - - /** - * Root paths from which the webview can load local (filesystem) resources using the `vscode-resource:` scheme. + * @param webviewPanel Webview panel to restore. The serializer should take ownership of this panel. + * @param state Persisted state. * - * Default to the root folders of the current workspace plus the extension's install directory. - * - * Pass in an empty array to disallow access to any local resources. + * @return Thanble indicating that the webview has been fully restored. */ - readonly localResourceRoots?: Uri[]; - } - - export interface WebViewOnDidChangeViewStateEvent { - readonly viewColumn: ViewColumn; - readonly active: boolean; - } - - /** - * A webview displays html content, like an iframe. - */ - export interface Webview { - /** - * The type of the webview, such as `'markdownw.preview'` - */ - readonly viewType: string; - - /** - * Content settings for the webview. - */ - readonly options: WebviewOptions; - - /** - * Title of the webview shown in UI. - */ - title: string; - - /** - * Contents of the webview. - * - * Should be a complete html document. - */ - html: string; - - /** - * The column in which the webview is showing. - */ - readonly viewColumn?: ViewColumn; - - /** - * Fired when the webview content posts a message. - */ - readonly onDidReceiveMessage: Event; - - /** - * Fired when the webview is disposed. - */ - readonly onDidDispose: Event; - - /** - * Fired when the webview's view state changes. - */ - readonly onDidChangeViewState: Event; - - /** - * Post a message to the webview content. - * - * Messages are only develivered if the webview is visible. - * - * @param message Body of the message. - */ - postMessage(message: any): Thenable; - - /** - * Shows the webview in a given column. - * - * A webview may only be in a single column at a time. If it is already showing, this - * command moves it to a new column. - */ - reveal(viewColumn: ViewColumn): void; - - /** - * Dispose of the the webview. - * - * This closes the webview if it showing and disposes of the resources owned by the webview. - * Webview are also disposed when the user closes the webview editor. Both cases fire `onDispose` - * event. Trying to use the webview after it has been disposed throws an exception. - */ - dispose(): any; + deserializeWebviewPanel(webviewPanel: WebviewPanel, state: any): Thenable; } namespace window { /** - * Create and show a new webview. + * Registers a webview panel serializer. * - * @param viewType Identifier the type of the webview. - * @param title Title of the webview. - * @param column Editor column to show the new webview in. - * @param options Content settings for the webview. + * Extensions that support reviving should have an `"onView:viewType"` activation method and + * make sure that [registerWebviewPanelSerializer](#registerWebviewPanelSerializer) is called during activation. + * + * Only a single serializer may be registered at a time for a given `viewType`. + * + * @param viewType Type of the webview panel that can be serialized. + * @param reviver Webview serializer. */ - export function createWebview(viewType: string, title: string, column: ViewColumn, options: WebviewOptions): Webview; - } - - //#endregion - - //#region Alex: TextEditor.visibleRange and related event - - export interface TextEditor { - /** - * The current visible ranges in the editor (vertically). - * This accounts only for vertical scrolling, and not for horizontal scrolling. - */ - readonly visibleRanges: Range[]; - } - - /** - * Represents an event describing the change in a [text editor's visible ranges](#TextEditor.visibleRanges). - */ - export interface TextEditorVisibleRangesChangeEvent { - /** - * The [text editor](#TextEditor) for which the visible ranges have changed. - */ - textEditor: TextEditor; - /** - * The new value for the [text editor's visible ranges](#TextEditor.visibleRanges). - */ - visibleRanges: Range[]; - } - - export namespace window { - /** - * An [event](#Event) which fires when the selection in an editor has changed. - */ - export const onDidChangeTextEditorVisibleRanges: Event; + export function registerWebviewPanelSerializer(viewType: string, reviver: WebviewPanelSerializer): Disposable; } //#endregion @@ -694,12 +544,25 @@ declare module 'vscode' { /** * An object representing an executed Task. It can be used * to terminate a task. + * + * This interface is not intended to be implemented. */ export interface TaskExecution { + /** + * The task that got started. + */ + task: Task; + + /** + * Terminates the task execution. + */ + terminate(): void; } /** * An event signaling the start of a task execution. + * + * This interface is not intended to be implemented. */ interface TaskStartEvent { /** @@ -710,6 +573,8 @@ declare module 'vscode' { /** * An event signaling the end of an executed task. + * + * This interface is not intended to be implemented. */ interface TaskEndEvent { /** @@ -718,14 +583,29 @@ declare module 'vscode' { execution: TaskExecution; } + export interface TaskFilter { + /** + * The task version as used in the tasks.json file. + * The string support the package.json semver notation. + */ + version?: string; + + /** + * The task type to return; + */ + type?: string; + } + export namespace workspace { /** - * Fetches all task available in the systems. This includes tasks + * Fetches all task available in the systems. Thisweweb includes tasks * from `tasks.json` files as well as tasks from task providers * contributed through extensions. + * + * @param filter a filter to filter the return tasks. */ - export function fetchTasks(): Thenable; + export function fetchTasks(filter?: TaskFilter): Thenable; /** * Executes a task that is managed by VS Code. The returned @@ -735,18 +615,18 @@ declare module 'vscode' { */ export function executeTask(task: Task): Thenable; + /** + * The currently active task executions or an empty array. + * + * @readonly + */ + export let taskExecutions: TaskExecution[]; + /** * Fires when a task starts. */ export const onDidStartTask: Event; - /** - * Terminates a task that was previously started using `executeTask` - * - * @param task the task to terminate - */ - export function terminateTask(task: TaskExecution): void; - /** * Fires when a task ends. */ @@ -754,4 +634,39 @@ declare module 'vscode' { } //#endregion + + //#region Terminal + + export namespace window { + /** + * The currently active terminals or an empty array. + * + * @readonly + */ + export let terminals: Terminal[]; + + /** + * An [event](#Event) which fires when a terminal has been created, either through the + * [createTerminal](#window.createTerminal) API or commands. + */ + export const onDidOpenTerminal: Event; + } + + //#endregion + + //#region URLs + + export interface ExternalUriHandler { + handleExternalUri(uri: Uri): void; + } + + export namespace window { + + /** + * Registers a protocol handler capable of handling system-wide URIs. + */ + export function registerExternalUriHandler(handler: ExternalUriHandler): Disposable; + } + + //#endregion } diff --git a/src/vs/workbench/api/browser/viewsContainersExtensionPoint.ts b/src/vs/workbench/api/browser/viewsContainersExtensionPoint.ts new file mode 100644 index 00000000000..36a9e67cbb1 --- /dev/null +++ b/src/vs/workbench/api/browser/viewsContainersExtensionPoint.ts @@ -0,0 +1,183 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import { localize } from 'vs/nls'; +import { IJSONSchema } from 'vs/base/common/jsonSchema'; +import { ExtensionMessageCollector, ExtensionsRegistry, IExtensionPoint } from 'vs/workbench/services/extensions/common/extensionsRegistry'; +import { join } from 'vs/base/common/paths'; +import { createCSSRule } from 'vs/base/browser/dom'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; +import { ViewletDescriptor, ViewletRegistry, Extensions as ViewletExtensions, ToggleViewletAction } from 'vs/workbench/browser/viewlet'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IPartService } from 'vs/workbench/services/part/common/partService'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { IStorageService } from 'vs/platform/storage/common/storage'; +import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IExtensionService, IExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; +import { ViewLocation } from 'vs/workbench/common/views'; +import { PersistentViewsViewlet } from 'vs/workbench/browser/parts/views/viewsViewlet'; +import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions'; +import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; +import { forEach } from 'vs/base/common/collections'; +import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; + + +export interface IUserFriendlyViewsContainerDescriptor { + id: string; + title: string; + icon: string; +} + +const viewsContainerSchema: IJSONSchema = { + type: 'object', + properties: { + id: { + description: localize('vscode.extension.contributes.views.containers.id', "Unique id used to identify the container in which views can be contributed using 'views' contribution point"), + type: 'string' + }, + label: { + description: localize('vscode.extension.contributes.views.containers.title', 'Human readable string used to render the container'), + type: 'string' + }, + icon: { + description: localize('vscode.extension.contributes.views.containers.icon', 'Path to the container icon'), + type: 'string' + } + } +}; + +export const viewsContainerContribution: IJSONSchema = { + description: localize('vscode.extension.contributes.viewsContainer', 'Contributes views containers to the editor'), + type: 'object', + properties: { + 'activitybar': { + description: localize('views.container.activitybar', "Contribute views containers to Activity Bar"), + type: 'array', + items: viewsContainerSchema + } + } +}; + +export const viewsContainersExtensionPoint: IExtensionPoint<{ [loc: string]: IUserFriendlyViewsContainerDescriptor[] }> = ExtensionsRegistry.registerExtensionPoint<{ [loc: string]: IUserFriendlyViewsContainerDescriptor[] }>('viewsContainers', [], viewsContainerContribution); +class ViewsContainersExtensionHandler implements IWorkbenchContribution { + + constructor() { + this.handleViewsContainersExtensionPoint(); + } + + private handleViewsContainersExtensionPoint() { + viewsContainersExtensionPoint.setHandler((extensions) => { + for (let extension of extensions) { + const { value, collector } = extension; + if (!extension.description.enableProposedApi) { + collector.error(localize('proposed', "'{0}' contribution is only available when running out of dev or with the following command line switch: --enable-proposed-api {1}", 'viewsContainer', extension.description.id)); + continue; + } + forEach(value, entry => { + if (!this.isValidViewsContainer(entry.value, collector)) { + return; + } + switch (entry.key) { + case 'activitybar': + this.contributeToActivitybar(entry.value, extension.description); + break; + } + }); + } + }); + } + + private isValidViewsContainer(viewsContainersDescriptors: IUserFriendlyViewsContainerDescriptor[], collector: ExtensionMessageCollector): boolean { + if (!Array.isArray(viewsContainersDescriptors)) { + collector.error(localize('requirearray', "views containers must be an array")); + return false; + } + + for (let descriptor of viewsContainersDescriptors) { + if (typeof descriptor.id !== 'string') { + collector.error(localize('requirestring', "property `{0}` is mandatory and must be of type `string`", 'id')); + return false; + } + if (typeof descriptor.title !== 'string') { + collector.error(localize('requirestring', "property `{0}` is mandatory and must be of type `string`", 'title')); + return false; + } + if (typeof descriptor.icon !== 'string') { + collector.error(localize('requirestring', "property `{0}` is mandatory and must be of type `string`", 'icon')); + return false; + } + } + + return true; + } + + private contributeToActivitybar(containers: IUserFriendlyViewsContainerDescriptor[], extension: IExtensionDescription) { + containers.forEach((descriptor, index) => { + const id = `workbench.view.extension.${descriptor.id}`; + const title = descriptor.title; + const cssClass = `extensionViewlet-${descriptor.id}`; + const location: ViewLocation = ViewLocation.register(id); + + // Generate CSS to show the icon in the activity bar + const iconClass = `.monaco-workbench > .activitybar .monaco-action-bar .action-label.${cssClass}`; + const iconPath = join(extension.extensionFolderPath, descriptor.icon); + createCSSRule(iconClass, `-webkit-mask: url('${iconPath}') no-repeat 50% 50%; -webkit-mask-size: 22px;`); + + // Register as viewlet + class CustomViewlet extends PersistentViewsViewlet { + constructor( + @IPartService partService: IPartService, + @ITelemetryService telemetryService: ITelemetryService, + @IWorkspaceContextService contextService: IWorkspaceContextService, + @IStorageService storageService: IStorageService, + @IWorkbenchEditorService editorService: IWorkbenchEditorService, + @IInstantiationService instantiationService: IInstantiationService, + @IContextKeyService contextKeyService: IContextKeyService, + @IThemeService themeService: IThemeService, + @IContextMenuService contextMenuService: IContextMenuService, + @IExtensionService extensionService: IExtensionService + ) { + super(id, location, `${id}.state`, true, partService, telemetryService, storageService, instantiationService, themeService, contextService, contextKeyService, contextMenuService, extensionService); + } + } + const viewletDescriptor = new ViewletDescriptor( + CustomViewlet, + id, + title, + cssClass, + 6 + index + ); + + Registry.as(ViewletExtensions.Viewlets).registerViewlet(viewletDescriptor); + + // Register Action to Open Viewlet + class OpenCustomViewletAction extends ToggleViewletAction { + constructor( + id: string, label: string, + @IViewletService viewletService: IViewletService, + @IWorkbenchEditorService editorService: IWorkbenchEditorService + ) { + super(id, label, id, viewletService, editorService); + } + } + const registry = Registry.as(ActionExtensions.WorkbenchActions); + registry.registerWorkbenchAction( + new SyncActionDescriptor(OpenCustomViewletAction, id, localize('showViewlet', "Show {0}", title)), + 'View: Show {0}', + localize('view', "View") + ); + }); + } +} + +const workbenchRegistry = Registry.as(WorkbenchExtensions.Workbench); +workbenchRegistry.registerWorkbenchContribution(ViewsContainersExtensionHandler, LifecyclePhase.Starting); \ No newline at end of file diff --git a/src/vs/workbench/api/browser/viewsExtensionPoint.ts b/src/vs/workbench/api/browser/viewsExtensionPoint.ts index 41a9e24e370..2931cf6e8dd 100644 --- a/src/vs/workbench/api/browser/viewsExtensionPoint.ts +++ b/src/vs/workbench/api/browser/viewsExtensionPoint.ts @@ -12,11 +12,10 @@ import { ViewLocation, ViewsRegistry, ICustomViewDescriptor } from 'vs/workbench import { CustomTreeViewPanel } from 'vs/workbench/browser/parts/views/customViewPanel'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { coalesce, } from 'vs/base/common/arrays'; +import { viewsContainersExtensionPoint } from 'vs/workbench/api/browser/viewsContainersExtensionPoint'; namespace schema { - // --views contribution point - export interface IUserFriendlyViewDescriptor { id: string; name: string; @@ -70,20 +69,43 @@ namespace schema { type: 'object', properties: { 'explorer': { - description: localize('views.explorer', "Explorer View"), + description: localize('views.explorer', "Contributes views to Explorer container in the Activity bar"), type: 'array', - items: viewDescriptor + items: viewDescriptor, + default: [] }, 'debug': { - description: localize('views.debug', "Debug View"), + description: localize('views.debug', "Contributes views to Debug container in the Activity bar"), type: 'array', - items: viewDescriptor + items: viewDescriptor, + default: [] + }, + 'scm': { + description: localize('views.scm', "Contributes views to SCM container in the Activity bar"), + type: 'array', + items: viewDescriptor, + default: [] } + }, + additionalProperties: { + description: localize('views.contributed', "Contributes views to contributed views container"), + type: 'array', + items: viewDescriptor, + default: [] } }; } -ExtensionsRegistry.registerExtensionPoint<{ [loc: string]: schema.IUserFriendlyViewDescriptor[] }>('views', [], schema.viewsContribution) +function getViewLocation(value: string): ViewLocation { + switch (value) { + case 'explorer': return ViewLocation.Explorer; + case 'debug': return ViewLocation.Debug; + case 'scm': return ViewLocation.SCM; + default: return ViewLocation.get(`workbench.view.extension.${value}`) || ViewLocation.Explorer; + } +} + +ExtensionsRegistry.registerExtensionPoint<{ [loc: string]: schema.IUserFriendlyViewDescriptor[] }>('views', [viewsContainersExtensionPoint], schema.viewsContribution) .setHandler((extensions) => { for (let extension of extensions) { const { value, collector } = extension; @@ -93,12 +115,7 @@ ExtensionsRegistry.registerExtensionPoint<{ [loc: string]: schema.IUserFriendlyV return; } - const location = ViewLocation.getContributedViewLocation(entry.key); - if (!location) { - collector.warn(localize('locationId.invalid', "`{0}` is not a valid view location", entry.key)); - return; - } - + const location = getViewLocation(entry.key); const registeredViews = ViewsRegistry.getViews(location); const viewIds = []; const viewDescriptors = coalesce(entry.value.map(item => { @@ -129,4 +146,4 @@ ExtensionsRegistry.registerExtensionPoint<{ [loc: string]: schema.IUserFriendlyV ViewsRegistry.registerViews(viewDescriptors); }); } - }); + }); \ No newline at end of file diff --git a/src/vs/workbench/api/electron-browser/extensionHost.contribution.ts b/src/vs/workbench/api/electron-browser/extensionHost.contribution.ts index 3418ddaf1a2..73eabaddf52 100644 --- a/src/vs/workbench/api/electron-browser/extensionHost.contribution.ts +++ b/src/vs/workbench/api/electron-browser/extensionHost.contribution.ts @@ -39,6 +39,7 @@ import './mainThreadOutputService'; import './mainThreadProgress'; import './mainThreadQuickOpen'; import './mainThreadSCM'; +import './mainThreadSearch'; import './mainThreadSaveParticipant'; import './mainThreadStatusBar'; import './mainThreadStorage'; @@ -48,6 +49,7 @@ import './mainThreadTerminalService'; import './mainThreadTreeViews'; import './mainThreadLogService'; import './mainThreadWebview'; +import './mainThreadUrls'; import './mainThreadWindow'; import './mainThreadWorkspace'; diff --git a/src/vs/workbench/api/electron-browser/mainThreadDebugService.ts b/src/vs/workbench/api/electron-browser/mainThreadDebugService.ts index 044af5fd345..893b8962517 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadDebugService.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadDebugService.ts @@ -6,7 +6,7 @@ import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import uri from 'vs/base/common/uri'; -import { IDebugService, IConfig, IDebugConfigurationProvider, IBreakpoint, IFunctionBreakpoint, IBreakpointData } from 'vs/workbench/parts/debug/common/debug'; +import { IDebugService, IConfig, IDebugConfigurationProvider, IBreakpoint, IFunctionBreakpoint, IBreakpointData, IAdapterExecutable, ITerminalSettings, IDebugAdapter, IDebugAdapterProvider } from 'vs/workbench/parts/debug/common/debug'; import { TPromise } from 'vs/base/common/winjs.base'; import { ExtHostContext, ExtHostDebugServiceShape, MainThreadDebugServiceShape, DebugSessionUUID, MainContext, @@ -14,13 +14,20 @@ import { } from '../node/extHost.protocol'; import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers'; import severity from 'vs/base/common/severity'; +import { AbstractDebugAdapter, convertToVSCPaths, convertToDAPaths } from 'vs/workbench/parts/debug/node/debugAdapter'; +import * as paths from 'vs/base/common/paths'; +import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; + @extHostNamedCustomer(MainContext.MainThreadDebugService) -export class MainThreadDebugService implements MainThreadDebugServiceShape { +export class MainThreadDebugService implements MainThreadDebugServiceShape, IDebugAdapterProvider { private _proxy: ExtHostDebugServiceShape; private _toDispose: IDisposable[]; private _breakpointEventsActive: boolean; + private _debugAdapters: Map; + private _debugAdaptersHandleCounter = 1; + constructor( extHostContext: IExtHostContext, @@ -46,6 +53,26 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape { } } })); + this._debugAdapters = new Map(); + } + + public $registerDebugTypes(debugTypes: string[]) { + this._toDispose.push(this.debugService.getConfigurationManager().registerDebugAdapterProvider(debugTypes, this)); + } + + createDebugAdapter(debugType: string, adapterInfo): IDebugAdapter { + const handle = this._debugAdaptersHandleCounter++; + const da = new ExtensionHostDebugAdapter(handle, this._proxy, debugType, adapterInfo); + this._debugAdapters.set(handle, da); + return da; + } + + substituteVariables(folder: IWorkspaceFolder, config: IConfig): TPromise { + return this._proxy.$substituteVariables(folder.uri, config); + } + + runInTerminal(args: DebugProtocol.RunInTerminalRequestArguments, config: ITerminalSettings): TPromise { + return this._proxy.$runInTerminal(args, config); } public dispose(): void { @@ -99,7 +126,7 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape { id: l.id, enabled: l.enabled, lineNumber: l.line + 1, - column: l.character > 0 ? l.character + 1 : 0, + column: l.character > 0 ? l.character + 1 : undefined, // a column value of 0 results in an omitted column attribute; see #46784 condition: l.condition, hitCondition: l.hitCondition, logMessage: l.logMessage @@ -119,7 +146,7 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape { return void 0; } - private convertToDto(bps: (IBreakpoint | IFunctionBreakpoint)[]): (ISourceBreakpointDto | IFunctionBreakpointDto)[] { + private convertToDto(bps: (ReadonlyArray)): (ISourceBreakpointDto | IFunctionBreakpointDto)[] { return bps.map(bp => { if ('name' in bp) { const fbp = bp; @@ -208,4 +235,62 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape { this.debugService.logToRepl(value, severity.Warning); return TPromise.wrap(undefined); } + + public $acceptDAMessage(handle: number, message: DebugProtocol.ProtocolMessage) { + + convertToVSCPaths(message, source => { + if (typeof source.path === 'object') { + source.path = uri.revive(source.path).toString(); + } + }); + + this._debugAdapters.get(handle).acceptMessage(message); + } + + public $acceptDAError(handle: number, name: string, message: string, stack: string) { + this._debugAdapters.get(handle).fireError(handle, new Error(`${name}: ${message}\n${stack}`)); + } + + public $acceptDAExit(handle: number, code: number, signal: string) { + this._debugAdapters.get(handle).fireExit(handle, code, signal); + } +} + +/** + * DebugAdapter that communicates via extension protocol with another debug adapter. + */ +class ExtensionHostDebugAdapter extends AbstractDebugAdapter { + + constructor(private _handle: number, private _proxy: ExtHostDebugServiceShape, private _debugType: string, private _adapterExecutable: IAdapterExecutable | null) { + super(); + } + + public fireError(handle: number, err: Error) { + this._onError.fire(err); + } + + public fireExit(handle: number, code: number, signal: string) { + this._onExit.fire(code); + } + + public startSession(): TPromise { + return this._proxy.$startDASession(this._handle, this._debugType, this._adapterExecutable); + } + + public sendMessage(message: DebugProtocol.ProtocolMessage): void { + + convertToDAPaths(message, source => { + if (paths.isAbsolute(source.path)) { + (source).path = uri.file(source.path); + } else { + (source).path = uri.parse(source.path); + } + }); + + this._proxy.$sendDAMessage(this._handle, message); + } + + public stopSession(): TPromise { + return this._proxy.$stopDASession(this._handle); + } } diff --git a/src/vs/workbench/api/electron-browser/mainThreadErrors.ts b/src/vs/workbench/api/electron-browser/mainThreadErrors.ts index a1ff2a2dd6c..6c283a0d98c 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadErrors.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadErrors.ts @@ -16,7 +16,7 @@ export class MainThreadErrors implements MainThreadErrorsShape { } $onUnexpectedError(err: any | SerializedError): void { - if (err.$isError) { + if (err && err.$isError) { const { name, message, stack } = err; err = new Error(); err.message = message; diff --git a/src/vs/workbench/api/electron-browser/mainThreadFileSystem.ts b/src/vs/workbench/api/electron-browser/mainThreadFileSystem.ts index df14909d35f..2f632a363e8 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadFileSystem.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadFileSystem.ts @@ -4,29 +4,23 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import URI, { UriComponents } from 'vs/base/common/uri'; -import { TPromise, PPromise } from 'vs/base/common/winjs.base'; -import { ExtHostContext, MainContext, IExtHostContext, MainThreadFileSystemShape, ExtHostFileSystemShape, IFileChangeDto } from '../node/extHost.protocol'; -import { IFileService, IFileSystemProvider, IStat, IFileChange } from 'vs/platform/files/common/files'; +import { Emitter, Event } from 'vs/base/common/event'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { Event, Emitter } from 'vs/base/common/event'; +import URI from 'vs/base/common/uri'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { FileOptions, FileSystemProviderCapabilities, IFileChange, IFileService, IFileSystemProvider, IStat, IWatchOptions } from 'vs/platform/files/common/files'; import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers'; -import { IProgress } from 'vs/platform/progress/common/progress'; -import { ISearchResultProvider, ISearchQuery, ISearchComplete, ISearchProgressItem, QueryType, IFileMatch, ISearchService, ILineMatch } from 'vs/platform/search/common/search'; -import { values } from 'vs/base/common/map'; -import { isFalsyOrEmpty } from 'vs/base/common/arrays'; +import { ExtHostContext, ExtHostFileSystemShape, IExtHostContext, IFileChangeDto, MainContext, MainThreadFileSystemShape } from '../node/extHost.protocol'; @extHostNamedCustomer(MainContext.MainThreadFileSystem) export class MainThreadFileSystem implements MainThreadFileSystemShape { private readonly _proxy: ExtHostFileSystemShape; private readonly _fileProvider = new Map(); - private readonly _searchProvider = new Map(); constructor( extHostContext: IExtHostContext, - @IFileService private readonly _fileService: IFileService, - @ISearchService private readonly _searchService: ISearchService + @IFileService private readonly _fileService: IFileService ) { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostFileSystem); } @@ -36,64 +30,36 @@ export class MainThreadFileSystem implements MainThreadFileSystemShape { this._fileProvider.clear(); } - $registerFileSystemProvider(handle: number, scheme: string): void { - this._fileProvider.set(handle, new RemoteFileSystemProvider(this._fileService, scheme, handle, this._proxy)); - } - - $registerSearchProvider(handle: number, scheme: string): void { - this._searchProvider.set(handle, new RemoteSearchProvider(this._searchService, scheme, handle, this._proxy)); + $registerFileSystemProvider(handle: number, scheme: string, capabilities: FileSystemProviderCapabilities): void { + this._fileProvider.set(handle, new RemoteFileSystemProvider(this._fileService, scheme, capabilities, handle, this._proxy)); } $unregisterProvider(handle: number): void { dispose(this._fileProvider.get(handle)); this._fileProvider.delete(handle); - - dispose(this._searchProvider.get(handle)); - this._searchProvider.delete(handle); } $onFileSystemChange(handle: number, changes: IFileChangeDto[]): void { this._fileProvider.get(handle).$onFileSystemChange(changes); } - - $reportFileChunk(handle: number, session: number, chunk: number[]): void { - this._fileProvider.get(handle).reportFileChunk(session, chunk); - } - - // --- search - - $handleFindMatch(handle: number, session, data: UriComponents | [UriComponents, ILineMatch]): void { - this._searchProvider.get(handle).handleFindMatch(session, data); - } -} - -class FileReadOperation { - - private static _idPool = 0; - - constructor( - readonly progress: IProgress, - readonly id: number = ++FileReadOperation._idPool - ) { - // - } } class RemoteFileSystemProvider implements IFileSystemProvider { private readonly _onDidChange = new Emitter(); private readonly _registrations: IDisposable[]; - private readonly _reads = new Map(); - - readonly onDidChange: Event = this._onDidChange.event; + readonly onDidChangeFile: Event = this._onDidChange.event; + readonly capabilities: FileSystemProviderCapabilities; constructor( fileService: IFileService, scheme: string, + capabilities: FileSystemProviderCapabilities, private readonly _handle: number, private readonly _proxy: ExtHostFileSystemShape ) { + this.capabilities = capabilities; this._registrations = [fileService.registerProvider(scheme, this)]; } @@ -102,6 +68,16 @@ class RemoteFileSystemProvider implements IFileSystemProvider { this._onDidChange.dispose(); } + watch(resource: URI, opts: IWatchOptions) { + const session = Math.random(); + this._proxy.$watch(this._handle, session, resource, opts); + return { + dispose: () => { + this._proxy.$unwatch(this._handle, session); + } + }; + } + $onFileSystemChange(changes: IFileChangeDto[]): void { this._onDidChange.fire(changes.map(RemoteFileSystemProvider._createFileChange)); } @@ -112,143 +88,42 @@ class RemoteFileSystemProvider implements IFileSystemProvider { // --- forwarding calls - utimes(resource: URI, mtime: number, atime: number): TPromise { - return this._proxy.$utimes(this._handle, resource, mtime, atime); - } - stat(resource: URI): TPromise { - return this._proxy.$stat(this._handle, resource); - } - read(resource: URI, offset: number, count: number, progress: IProgress): TPromise { - const read = new FileReadOperation(progress); - this._reads.set(read.id, read); - return this._proxy.$read(this._handle, read.id, offset, count, resource).then(value => { - this._reads.delete(read.id); - return value; + stat(resource: URI): TPromise { + return this._proxy.$stat(this._handle, resource).then(undefined, err => { + throw err; }); } - reportFileChunk(session: number, chunk: number[]): void { - this._reads.get(session).progress.report(Buffer.from(chunk)); + + readFile(resource: URI, opts: FileOptions): TPromise { + return this._proxy.$readFile(this._handle, resource, opts).then(encoded => { + return Buffer.from(encoded, 'base64'); + }); } - write(resource: URI, content: Uint8Array): TPromise { - return this._proxy.$write(this._handle, resource, [].slice.call(content)); + + writeFile(resource: URI, content: Uint8Array, opts: FileOptions): TPromise { + let encoded = Buffer.isBuffer(content) + ? content.toString('base64') + : Buffer.from(content.buffer, content.byteOffset, content.byteLength).toString('base64'); + return this._proxy.$writeFile(this._handle, resource, encoded, opts); } - unlink(resource: URI): TPromise { - return this._proxy.$unlink(this._handle, resource); - } - move(resource: URI, target: URI): TPromise { - return this._proxy.$move(this._handle, resource, target); + + delete(resource: URI): TPromise { + return this._proxy.$delete(this._handle, resource); } + mkdir(resource: URI): TPromise { return this._proxy.$mkdir(this._handle, resource); } - readdir(resource: URI): TPromise<[URI, IStat][], any> { - return this._proxy.$readdir(this._handle, resource).then(data => { - return data.map(tuple => <[URI, IStat]>[URI.revive(tuple[0]), tuple[1]]); - }); + + readdir(resource: URI): TPromise<[string, IStat][], any> { + return this._proxy.$readdir(this._handle, resource); } - rmdir(resource: URI): TPromise { - return this._proxy.$rmdir(this._handle, resource); - } -} - -class SearchOperation { - - private static _idPool = 0; - - constructor( - readonly progress: (match: IFileMatch) => any, - readonly id: number = ++SearchOperation._idPool, - readonly matches = new Map() - ) { - // - } - - addMatch(resource: URI, match: ILineMatch): void { - if (!this.matches.has(resource.toString())) { - this.matches.set(resource.toString(), { resource, lineMatches: [] }); - } - if (match) { - this.matches.get(resource.toString()).lineMatches.push(match); - } - this.progress(this.matches.get(resource.toString())); - } -} - -class RemoteSearchProvider implements ISearchResultProvider { - - private readonly _registrations: IDisposable[]; - private readonly _searches = new Map(); - - - constructor( - searchService: ISearchService, - private readonly _scheme: string, - private readonly _handle: number, - private readonly _proxy: ExtHostFileSystemShape - ) { - this._registrations = [searchService.registerSearchResultProvider(this)]; - } - - dispose(): void { - dispose(this._registrations); - } - - search(query: ISearchQuery): PPromise { - - if (isFalsyOrEmpty(query.folderQueries)) { - return PPromise.as(undefined); - } - - let includes = { ...query.includePattern }; - let excludes = { ...query.excludePattern }; - - for (const folderQuery of query.folderQueries) { - if (folderQuery.folder.scheme === this._scheme) { - includes = { ...includes, ...folderQuery.includePattern }; - excludes = { ...excludes, ...folderQuery.excludePattern }; - } - } - - let outer: TPromise; - - return new PPromise((resolve, reject, report) => { - - const search = new SearchOperation(report); - this._searches.set(search.id, search); - - outer = query.type === QueryType.File - ? this._proxy.$provideFileSearchResults(this._handle, search.id, query.filePattern) - : this._proxy.$provideTextSearchResults(this._handle, search.id, query.contentPattern, { excludes: Object.keys(excludes), includes: Object.keys(includes) }); - - outer.then(() => { - this._searches.delete(search.id); - resolve(({ results: values(search.matches), stats: undefined })); - }, err => { - this._searches.delete(search.id); - reject(err); - }); - }, () => { - if (outer) { - outer.cancel(); - } - }); - } - - handleFindMatch(session: number, dataOrUri: UriComponents | [UriComponents, ILineMatch]): void { - if (!this._searches.has(session)) { - // ignore... - return; - } - let resource: URI; - let match: ILineMatch; - - if (Array.isArray(dataOrUri)) { - resource = URI.revive(dataOrUri[0]); - match = dataOrUri[1]; - } else { - resource = URI.revive(dataOrUri); - } - - this._searches.get(session).addMatch(resource, match); + + rename(resource: URI, target: URI, opts: FileOptions): TPromise { + return this._proxy.$rename(this._handle, resource, target, opts); + } + + copy(resource: URI, target: URI, opts: FileOptions): TPromise { + return this._proxy.$copy(this._handle, resource, target, opts); } } diff --git a/src/vs/workbench/api/electron-browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/electron-browser/mainThreadLanguageFeatures.ts index c4c01b90d41..34fb61eea6a 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadLanguageFeatures.ts @@ -191,11 +191,12 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha // --- quick fix - $registerQuickFixSupport(handle: number, selector: ISerializedDocumentFilter[]): void { + $registerQuickFixSupport(handle: number, selector: ISerializedDocumentFilter[], providedCodeActionKinds?: string[]): void { this._registrations[handle] = modes.CodeActionProviderRegistry.register(toLanguageSelector(selector), { provideCodeActions: (model: ITextModel, range: EditorRange, context: modes.CodeActionContext, token: CancellationToken): Thenable => { return this._heapService.trackRecursive(wireCancellationToken(token, this._proxy.$provideCodeActions(handle, model.uri, range, context))).then(MainThreadLanguageFeatures._reviveCodeActionDto); - } + }, + providedCodeActionKinds }); } @@ -257,7 +258,7 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha return wireCancellationToken(token, this._proxy.$provideRenameEdits(handle, model.uri, position, newName)).then(reviveWorkspaceEditDto); }, resolveRenameLocation: supportResolveLocation - ? (model: ITextModel, position: EditorPosition, token: CancellationToken): Thenable => wireCancellationToken(token, this._proxy.$resolveRenameLocation(handle, model.uri, position)) + ? (model: ITextModel, position: EditorPosition, token: CancellationToken): Thenable => wireCancellationToken(token, this._proxy.$resolveRenameLocation(handle, model.uri, position)) : undefined }); } @@ -348,9 +349,9 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha // --- folding - $registerFoldingProvider(handle: number, selector: ISerializedDocumentFilter[]): void { + $registerFoldingRangeProvider(handle: number, selector: ISerializedDocumentFilter[]): void { const proxy = this._proxy; - this._registrations[handle] = modes.FoldingProviderRegistry.register(toLanguageSelector(selector), { + this._registrations[handle] = modes.FoldingRangeProviderRegistry.register(toLanguageSelector(selector), { provideFoldingRanges: (model, context, token) => { return wireCancellationToken(token, proxy.$provideFoldingRanges(handle, model.uri, context)); } diff --git a/src/vs/workbench/api/electron-browser/mainThreadMessageService.ts b/src/vs/workbench/api/electron-browser/mainThreadMessageService.ts index 016ebbf355a..92ca4c3cb44 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadMessageService.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadMessageService.ts @@ -92,7 +92,7 @@ export class MainThreadMessageService implements MainThreadMessageServiceShape { // if promise has not been resolved yet, now is the time to ensure a return value // otherwise if already resolved it means the user clicked one of the buttons - once(messageHandle.onDidDispose)(() => { + once(messageHandle.onDidClose)(() => { dispose(...primaryActions, ...secondaryActions); resolve(undefined); }); diff --git a/src/vs/workbench/api/electron-browser/mainThreadQuickOpen.ts b/src/vs/workbench/api/electron-browser/mainThreadQuickOpen.ts index 7ac56ff2c63..c84d6a8e5f3 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadQuickOpen.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadQuickOpen.ts @@ -7,6 +7,7 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { asWinJsPromise } from 'vs/base/common/async'; import { IQuickOpenService, IPickOptions, IInputOptions } from 'vs/platform/quickOpen/common/quickOpen'; +import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { InputBoxOptions } from 'vscode'; import { ExtHostContext, MainThreadQuickOpenShape, ExtHostQuickOpenShape, MyQuickPickItems, MainContext, IExtHostContext } from '../node/extHost.protocol'; import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers'; @@ -16,6 +17,7 @@ export class MainThreadQuickOpen implements MainThreadQuickOpenShape { private _proxy: ExtHostQuickOpenShape; private _quickOpenService: IQuickOpenService; + private _quickInputService: IQuickInputService; private _doSetItems: (items: MyQuickPickItems[]) => any; private _doSetError: (error: Error) => any; private _contents: TPromise; @@ -23,16 +25,18 @@ export class MainThreadQuickOpen implements MainThreadQuickOpenShape { constructor( extHostContext: IExtHostContext, - @IQuickOpenService quickOpenService: IQuickOpenService + @IQuickOpenService quickOpenService: IQuickOpenService, + @IQuickInputService quickInputService: IQuickInputService ) { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostQuickOpen); this._quickOpenService = quickOpenService; + this._quickInputService = quickInputService; } public dispose(): void { } - $show(options: IPickOptions): TPromise { + $show(options: IPickOptions): TPromise { const myToken = ++this._token; @@ -50,16 +54,29 @@ export class MainThreadQuickOpen implements MainThreadQuickOpenShape { }; }); - return asWinJsPromise(token => this._quickOpenService.pick(this._contents, options, token)).then(item => { - if (item) { - return item.handle; - } - return undefined; - }, undefined, progress => { - if (progress) { - this._proxy.$onItemSelected((progress).handle); - } - }); + if (options.canSelectMany) { + return asWinJsPromise(token => this._quickInputService.pick(this._contents, options, token)).then(items => { + if (items) { + return items.map(item => item.handle); + } + return undefined; + }, undefined, progress => { + if (progress) { + this._proxy.$onItemSelected((progress).handle); + } + }); + } else { + return asWinJsPromise(token => this._quickOpenService.pick(this._contents, options, token)).then(item => { + if (item) { + return item.handle; + } + return undefined; + }, undefined, progress => { + if (progress) { + this._proxy.$onItemSelected((progress).handle); + } + }); + } } $setItems(items: MyQuickPickItems[]): TPromise { diff --git a/src/vs/workbench/api/electron-browser/mainThreadSaveParticipant.ts b/src/vs/workbench/api/electron-browser/mainThreadSaveParticipant.ts index 33b9c3e3239..c5c267edb8d 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadSaveParticipant.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadSaveParticipant.ts @@ -29,6 +29,7 @@ import { localize } from 'vs/nls'; import { isFalsyOrEmpty } from 'vs/base/common/arrays'; import { ILogService } from 'vs/platform/log/common/log'; import { shouldSynchronizeModel } from 'vs/editor/common/services/modelService'; +import { SnippetController2 } from 'vs/editor/contrib/snippet/snippetController2'; export interface ISaveParticipantParticipant extends ISaveParticipant { // progressMessage: string; @@ -50,7 +51,7 @@ class TrimWhitespaceParticipant implements ISaveParticipantParticipant { } private doTrimTrailingWhitespace(model: ITextModel, isAutoSaved: boolean): void { - let prevSelection: Selection[] = [new Selection(1, 1, 1, 1)]; + let prevSelection: Selection[] = []; const cursors: Position[] = []; let editor = findEditor(model, this.codeEditorService); @@ -60,6 +61,12 @@ class TrimWhitespaceParticipant implements ISaveParticipantParticipant { prevSelection = editor.getSelections(); if (isAutoSaved) { cursors.push(...prevSelection.map(s => new Position(s.positionLineNumber, s.positionColumn))); + const snippetsRange = SnippetController2.get(editor).getSessionEnclosingRange(); + if (snippetsRange) { + for (let lineNumber = snippetsRange.startLineNumber; lineNumber <= snippetsRange.endLineNumber; lineNumber++) { + cursors.push(new Position(lineNumber, model.getLineMaxColumn(lineNumber))); + } + } } } @@ -114,7 +121,7 @@ export class FinalNewLineParticipant implements ISaveParticipantParticipant { return; } - let prevSelection: Selection[] = [new Selection(1, 1, 1, 1)]; + let prevSelection: Selection[] = []; const editor = findEditor(model, this.codeEditorService); if (editor) { prevSelection = editor.getSelections(); @@ -151,7 +158,7 @@ export class TrimFinalNewLinesParticipant implements ISaveParticipantParticipant return; } - let prevSelection: Selection[] = [new Selection(1, 1, 1, 1)]; + let prevSelection: Selection[] = []; const editor = findEditor(model, this.codeEditorService); if (editor) { prevSelection = editor.getSelections(); @@ -166,7 +173,7 @@ export class TrimFinalNewLinesParticipant implements ISaveParticipantParticipant currentLineIsEmptyOrWhitespace = strings.lastNonWhitespaceIndex(currentLine) === -1; } - const deletionRange = new Range(currentLineNumber + 1, 1, lineCount + 1, 1); + const deletionRange = model.validateRange(new Range(currentLineNumber + 1, 1, lineCount + 1, 1)); if (!deletionRange.isEmpty()) { model.pushEditOperations(prevSelection, [EditOperation.delete(deletionRange)], edits => prevSelection); } @@ -225,7 +232,7 @@ class FormatOnSaveParticipant implements ISaveParticipantParticipant { } private _editsWithEditor(editor: ICodeEditor, edits: ISingleEditOperation[]): void { - EditOperationsCommand.execute(editor, edits, false); + EditOperationsCommand.execute(editor, edits); } private _editWithModel(model: ITextModel, edits: ISingleEditOperation[]): void { diff --git a/src/vs/workbench/api/electron-browser/mainThreadSearch.ts b/src/vs/workbench/api/electron-browser/mainThreadSearch.ts new file mode 100644 index 00000000000..e83864d3a4a --- /dev/null +++ b/src/vs/workbench/api/electron-browser/mainThreadSearch.ts @@ -0,0 +1,148 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { isFalsyOrEmpty } from 'vs/base/common/arrays'; +import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { values } from 'vs/base/common/map'; +import URI, { UriComponents } from 'vs/base/common/uri'; +import { PPromise, TPromise } from 'vs/base/common/winjs.base'; +import { IFileMatch, ILineMatch, ISearchComplete, ISearchProgressItem, ISearchQuery, ISearchResultProvider, ISearchService, QueryType } from 'vs/platform/search/common/search'; +import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers'; +import { ExtHostContext, ExtHostSearchShape, IExtHostContext, MainContext, MainThreadSearchShape } from '../node/extHost.protocol'; + +@extHostNamedCustomer(MainContext.MainThreadSearch) +export class MainThreadSearch implements MainThreadSearchShape { + + private readonly _proxy: ExtHostSearchShape; + private readonly _searchProvider = new Map(); + + constructor( + extHostContext: IExtHostContext, + @ISearchService private readonly _searchService: ISearchService + ) { + this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostSearch); + } + + dispose(): void { + this._searchProvider.forEach(value => dispose()); + this._searchProvider.clear(); + } + + $registerSearchProvider(handle: number, scheme: string): void { + this._searchProvider.set(handle, new RemoteSearchProvider(this._searchService, scheme, handle, this._proxy)); + } + + $unregisterProvider(handle: number): void { + dispose(this._searchProvider.get(handle)); + this._searchProvider.delete(handle); + } + + $handleFindMatch(handle: number, session, data: UriComponents | [UriComponents, ILineMatch]): void { + this._searchProvider.get(handle).handleFindMatch(session, data); + } +} + +class SearchOperation { + + private static _idPool = 0; + + constructor( + readonly progress: (match: IFileMatch) => any, + readonly id: number = ++SearchOperation._idPool, + readonly matches = new Map() + ) { + // + } + + addMatch(resource: URI, match: ILineMatch): void { + if (!this.matches.has(resource.toString())) { + this.matches.set(resource.toString(), { resource, lineMatches: [] }); + } + if (match) { + this.matches.get(resource.toString()).lineMatches.push(match); + } + this.progress(this.matches.get(resource.toString())); + } +} + +class RemoteSearchProvider implements ISearchResultProvider { + + private readonly _registrations: IDisposable[]; + private readonly _searches = new Map(); + + + constructor( + searchService: ISearchService, + private readonly _scheme: string, + private readonly _handle: number, + private readonly _proxy: ExtHostSearchShape + ) { + this._registrations = [searchService.registerSearchResultProvider(this)]; + } + + dispose(): void { + dispose(this._registrations); + } + + search(query: ISearchQuery): PPromise { + + if (isFalsyOrEmpty(query.folderQueries)) { + return PPromise.as(undefined); + } + + let includes = { ...query.includePattern }; + let excludes = { ...query.excludePattern }; + + for (const folderQuery of query.folderQueries) { + if (folderQuery.folder.scheme === this._scheme) { + includes = { ...includes, ...folderQuery.includePattern }; + excludes = { ...excludes, ...folderQuery.excludePattern }; + } + } + + let outer: TPromise; + + return new PPromise((resolve, reject, report) => { + + const search = new SearchOperation(report); + this._searches.set(search.id, search); + + outer = query.type === QueryType.File + ? this._proxy.$provideFileSearchResults(this._handle, search.id, query.filePattern) + : this._proxy.$provideTextSearchResults(this._handle, search.id, query.contentPattern, { excludes: Object.keys(excludes), includes: Object.keys(includes) }); + + outer.then(() => { + this._searches.delete(search.id); + resolve(({ results: values(search.matches), stats: undefined })); + }, err => { + this._searches.delete(search.id); + reject(err); + }); + }, () => { + if (outer) { + outer.cancel(); + } + }); + } + + handleFindMatch(session: number, dataOrUri: UriComponents | [UriComponents, ILineMatch]): void { + if (!this._searches.has(session)) { + // ignore... + return; + } + let resource: URI; + let match: ILineMatch; + + if (Array.isArray(dataOrUri)) { + resource = URI.revive(dataOrUri[0]); + match = dataOrUri[1]; + } else { + resource = URI.revive(dataOrUri); + } + + this._searches.get(session).addMatch(resource, match); + } +} diff --git a/src/vs/workbench/api/electron-browser/mainThreadTask.ts b/src/vs/workbench/api/electron-browser/mainThreadTask.ts index 03fa8d2b3b8..cd32296c9da 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadTask.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadTask.ts @@ -17,29 +17,31 @@ import { IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspac import { ContributedTask, ExtensionTaskSourceTransfer, TaskIdentifier, TaskExecution, Task, TaskEvent, TaskEventKind, - PresentationOptions, CommandOptions, CommandConfiguration, RuntimeType, CustomTask, TaskScope, TaskSource, TaskSourceKind, ExtensionTaskSource + PresentationOptions, CommandOptions, CommandConfiguration, RuntimeType, CustomTask, TaskScope, TaskSource, TaskSourceKind, ExtensionTaskSource, RevealKind, PanelKind } from 'vs/workbench/parts/tasks/common/tasks'; -import { ITaskService } from 'vs/workbench/parts/tasks/common/taskService'; +import { ITaskService, TaskFilter } from 'vs/workbench/parts/tasks/common/taskService'; import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers'; import { ExtHostContext, MainThreadTaskShape, ExtHostTaskShape, MainContext, IExtHostContext } from 'vs/workbench/api/node/extHost.protocol'; import { TaskDefinitionDTO, TaskExecutionDTO, ProcessExecutionOptionsDTO, TaskPresentationOptionsDTO, - ProcessExecutionDTO, ShellExecutionDTO, ShellExecutionOptionsDTO, TaskDTO, TaskSourceDTO, TaskHandleDTO + ProcessExecutionDTO, ShellExecutionDTO, ShellExecutionOptionsDTO, TaskDTO, TaskSourceDTO, TaskHandleDTO, TaskFilterDTO } from 'vs/workbench/api/shared/tasks'; -export { TaskDTO, TaskHandleDTO, TaskExecutionDTO }; +export { TaskDTO, TaskHandleDTO, TaskExecutionDTO, TaskFilterDTO }; namespace TaskExecutionDTO { export function from(value: TaskExecution): TaskExecutionDTO { return { id: value.id, + task: TaskDTO.from(value.task) }; } export function to(value: TaskExecutionDTO, workspace: IWorkspaceContextService): TaskExecution { return { id: value.id, + task: TaskDTO.to(value.task, workspace) }; } } @@ -303,6 +305,8 @@ namespace TaskDTO { return undefined; } command.presentation = TaskPresentationOptionsDTO.to(task.presentationOptions); + command.presentation = Objects.assign(command.presentation || {}, { echo: true, reveal: RevealKind.Always, focus: false, panel: PanelKind.Shared }); + let source = TaskSourceDTO.to(task.source, workspace); let label = nls.localize('task.label', '{0}: {1}', source.label, task.name); @@ -326,6 +330,15 @@ namespace TaskDTO { } } +namespace TaskFilterDTO { + export function from(value: TaskFilter): TaskFilterDTO { + return value; + } + export function to(value: TaskFilterDTO): TaskFilter { + return value; + } +} + @extHostNamedCustomer(MainContext.MainThreadTask) export class MainThreadTask implements MainThreadTaskShape { @@ -383,8 +396,8 @@ export class MainThreadTask implements MainThreadTaskShape { return TPromise.wrap(undefined); } - public $executeTaskProvider(): TPromise { - return this._taskService.tasks().then((tasks) => { + public $fetchTasks(filter?: TaskFilterDTO): TPromise { + return this._taskService.tasks(TaskFilterDTO.to(filter)).then((tasks) => { let result: TaskDTO[] = []; for (let task of tasks) { let item = TaskDTO.from(task); @@ -403,7 +416,8 @@ export class MainThreadTask implements MainThreadTaskShape { this._taskService.getTask(workspaceFolder, value.id, true).then((task: Task) => { this._taskService.run(task); let result: TaskExecutionDTO = { - id: value.id + id: value.id, + task: TaskDTO.from(task) }; resolve(result); }, (error) => { @@ -413,19 +427,19 @@ export class MainThreadTask implements MainThreadTaskShape { let task = TaskDTO.to(value, this._workspaceContextServer); this._taskService.run(task); let result: TaskExecutionDTO = { - id: task._id + id: task._id, + task: TaskDTO.from(task) }; resolve(result); } }); } - public $terminateTask(value: TaskExecutionDTO): TPromise { - let execution: TaskExecution = TaskExecutionDTO.to(value, this._workspaceContextServer); + public $terminateTask(id: string): TPromise { return new TPromise((resolve, reject) => { this._taskService.getActiveTasks().then((tasks) => { for (let task of tasks) { - if (execution.id === task._id) { + if (id === task._id) { this._taskService.terminate(task).then((value) => { resolve(undefined); }, (error) => { diff --git a/src/vs/workbench/api/electron-browser/mainThreadTerminalService.ts b/src/vs/workbench/api/electron-browser/mainThreadTerminalService.ts index ee934a107b5..434652bfaf6 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadTerminalService.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadTerminalService.ts @@ -5,25 +5,38 @@ 'use strict'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { ITerminalService, ITerminalInstance, IShellLaunchConfig } from 'vs/workbench/parts/terminal/common/terminal'; +import { ITerminalService, ITerminalInstance, IShellLaunchConfig, ITerminalProcessExtHostProxy, ITerminalProcessExtHostRequest } from 'vs/workbench/parts/terminal/common/terminal'; import { TPromise } from 'vs/base/common/winjs.base'; -import { ExtHostContext, ExtHostTerminalServiceShape, MainThreadTerminalServiceShape, MainContext, IExtHostContext } from '../node/extHost.protocol'; +import { ExtHostContext, ExtHostTerminalServiceShape, MainThreadTerminalServiceShape, MainContext, IExtHostContext, ShellLaunchConfigDto } from '../node/extHost.protocol'; import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers'; @extHostNamedCustomer(MainContext.MainThreadTerminalService) export class MainThreadTerminalService implements MainThreadTerminalServiceShape { private _proxy: ExtHostTerminalServiceShape; - private _toDispose: IDisposable[]; + private _toDispose: IDisposable[] = []; + private _terminalProcesses: { [id: number]: ITerminalProcessExtHostProxy } = {}; constructor( extHostContext: IExtHostContext, @ITerminalService private terminalService: ITerminalService ) { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostTerminalService); - this._toDispose = []; - this._toDispose.push(terminalService.onInstanceDisposed((terminalInstance) => this._onTerminalDisposed(terminalInstance))); - this._toDispose.push(terminalService.onInstanceProcessIdReady((terminalInstance) => this._onTerminalProcessIdReady(terminalInstance))); + this._toDispose.push(terminalService.onInstanceCreated((terminalInstance) => { + // Delay this message so the TerminalInstance constructor has a chance to finish and + // return the ID normally to the extension host. The ID that is passed here will be used + // to register non-extension API terminals in the extension host. + setTimeout(() => this._onTerminalOpened(terminalInstance), 100); + })); + this._toDispose.push(terminalService.onInstanceDisposed(terminalInstance => this._onTerminalDisposed(terminalInstance))); + this._toDispose.push(terminalService.onInstanceProcessIdReady(terminalInstance => this._onTerminalProcessIdReady(terminalInstance))); + this._toDispose.push(terminalService.onInstanceRequestExtHostProcess(request => this._onTerminalRequestExtHostProcess(request))); + + // Set initial ext host state + this.terminalService.terminalInstances.forEach(t => { + this._onTerminalOpened(t); + t.processReady.then(() => this._onTerminalProcessIdReady(t)); + }); } public dispose(): void { @@ -43,7 +56,7 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape ignoreConfigurationCwd: true, env }; - return TPromise.as(this.terminalService.createInstance(shellLaunchConfig).id); + return TPromise.as(this.terminalService.createTerminal(shellLaunchConfig).id); } public $show(terminalId: number, preserveFocus: boolean): void { @@ -78,7 +91,43 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape this._proxy.$acceptTerminalClosed(terminalInstance.id); } + private _onTerminalOpened(terminalInstance: ITerminalInstance): void { + this._proxy.$acceptTerminalOpened(terminalInstance.id, terminalInstance.title); + } + private _onTerminalProcessIdReady(terminalInstance: ITerminalInstance): void { this._proxy.$acceptTerminalProcessId(terminalInstance.id, terminalInstance.processId); } + + private _onTerminalRequestExtHostProcess(request: ITerminalProcessExtHostRequest): void { + this._terminalProcesses[request.proxy.terminalId] = request.proxy; + const shellLaunchConfigDto: ShellLaunchConfigDto = { + name: request.shellLaunchConfig.name, + executable: request.shellLaunchConfig.executable, + args: request.shellLaunchConfig.args, + cwd: request.shellLaunchConfig.cwd, + env: request.shellLaunchConfig.env + }; + this._proxy.$createProcess(request.proxy.terminalId, shellLaunchConfigDto, request.cols, request.rows); + request.proxy.onInput(data => this._proxy.$acceptProcessInput(request.proxy.terminalId, data)); + request.proxy.onResize((cols, rows) => this._proxy.$acceptProcessResize(request.proxy.terminalId, cols, rows)); + request.proxy.onShutdown(() => this._proxy.$acceptProcessShutdown(request.proxy.terminalId)); + } + + public $sendProcessTitle(terminalId: number, title: string): void { + this._terminalProcesses[terminalId].emitTitle(title); + } + + public $sendProcessData(terminalId: number, data: string): void { + this._terminalProcesses[terminalId].emitData(data); + } + + public $sendProcessPid(terminalId: number, pid: number): void { + this._terminalProcesses[terminalId].emitPid(pid); + } + + public $sendProcessExit(terminalId: number, exitCode: number): void { + this._terminalProcesses[terminalId].emitExit(exitCode); + delete this._terminalProcesses[terminalId]; + } } diff --git a/src/vs/workbench/api/electron-browser/mainThreadTreeViews.ts b/src/vs/workbench/api/electron-browser/mainThreadTreeViews.ts index 65b4f04f3ac..828bd6e2e33 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadTreeViews.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadTreeViews.ts @@ -31,7 +31,12 @@ export class MainThreadTreeViews extends Disposable implements MainThreadTreeVie $registerTreeViewDataProvider(treeViewId: string): void { const dataProvider = this._register(new TreeViewDataProvider(treeViewId, this._proxy, this.notificationService)); this._dataProviders.set(treeViewId, dataProvider); - this.viewsService.getTreeViewer(treeViewId).dataProvider = dataProvider; + const treeViewer = this.viewsService.getTreeViewer(treeViewId); + if (treeViewer) { + treeViewer.dataProvider = dataProvider; + } else { + this.notificationService.error('No view is registered with id: ' + treeViewId); + } } $reveal(treeViewId: string, item: ITreeItem, parentChain: ITreeItem[], options?: { select?: boolean }): TPromise { diff --git a/src/vs/workbench/api/electron-browser/mainThreadUrls.ts b/src/vs/workbench/api/electron-browser/mainThreadUrls.ts new file mode 100644 index 00000000000..1923e437eb1 --- /dev/null +++ b/src/vs/workbench/api/electron-browser/mainThreadUrls.ts @@ -0,0 +1,75 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ExtHostContext, IExtHostContext, MainContext, MainThreadUrlsShape, ExtHostUrlsShape } from 'vs/workbench/api/node/extHost.protocol'; +import { extHostNamedCustomer } from './extHostCustomers'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { IURLService, IURLHandler } from 'vs/platform/url/common/url'; +import URI from 'vs/base/common/uri'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import { IInactiveExtensionUrlHandler } from 'vs/platform/url/electron-browser/inactiveExtensionUrlHandler'; + +class ExtensionUrlHandler implements IURLHandler { + + constructor( + private readonly proxy: ExtHostUrlsShape, + private readonly handle: number, + readonly extensionId: string + ) { } + + handleURL(uri: URI): TPromise { + if (uri.authority !== this.extensionId) { + return TPromise.as(false); + } + + return this.proxy.$handleExternalUri(this.handle, uri).then(() => true); + } +} + +@extHostNamedCustomer(MainContext.MainThreadUrls) +export class MainThreadUrls implements MainThreadUrlsShape { + + private readonly proxy: ExtHostUrlsShape; + private handlers = new Map(); + + constructor( + context: IExtHostContext, + @IURLService private urlService: IURLService, + @IInactiveExtensionUrlHandler private inactiveExtensionUrlHandler: IInactiveExtensionUrlHandler + ) { + this.proxy = context.getProxy(ExtHostContext.ExtHostUrls); + } + + $registerExternalUriHandler(handle: number, extensionId: string): TPromise { + const handler = new ExtensionUrlHandler(this.proxy, handle, extensionId); + const disposable = this.urlService.registerHandler(handler); + + this.handlers.set(handle, { extensionId, disposable }); + this.inactiveExtensionUrlHandler.registerExtensionHandler(extensionId, handler); + + return TPromise.as(null); + } + + $unregisterExternalUriHandler(handle: number): TPromise { + const tuple = this.handlers.get(handle); + + if (!tuple) { + return TPromise.as(null); + } + + const { extensionId, disposable } = tuple; + + this.inactiveExtensionUrlHandler.unregisterExtensionHandler(extensionId); + this.handlers.delete(handle); + disposable.dispose(); + + return TPromise.as(null); + } + + dispose(): void { + this.handlers.forEach(({ disposable }) => disposable.dispose()); + this.handlers.clear(); + } +} diff --git a/src/vs/workbench/api/electron-browser/mainThreadWebview.ts b/src/vs/workbench/api/electron-browser/mainThreadWebview.ts index aba0f0c1621..501549b3bbc 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadWebview.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadWebview.ts @@ -2,106 +2,112 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ - +import { localize } from 'vs/nls'; +import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import * as map from 'vs/base/common/map'; -import { MainThreadWebviewsShape, MainContext, IExtHostContext, ExtHostContext, ExtHostWebviewsShape, WebviewHandle } from 'vs/workbench/api/node/extHost.protocol'; -import { dispose, Disposable } from 'vs/base/common/lifecycle'; -import { extHostNamedCustomer } from './extHostCustomers'; -import { Position } from 'vs/platform/editor/common/editor'; -import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { IPartService } from 'vs/workbench/services/part/common/partService'; -import { IOpenerService } from 'vs/platform/opener/common/opener'; -import * as vscode from 'vscode'; -import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService'; import URI from 'vs/base/common/uri'; -import { WebviewInput } from 'vs/workbench/parts/webview/electron-browser/webviewInput'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { Position } from 'vs/platform/editor/common/editor'; +import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { ExtHostContext, ExtHostWebviewsShape, IExtHostContext, MainContext, MainThreadWebviewsShape, WebviewPanelHandle } from 'vs/workbench/api/node/extHost.protocol'; import { WebviewEditor } from 'vs/workbench/parts/webview/electron-browser/webviewEditor'; - +import { WebviewEditorInput } from 'vs/workbench/parts/webview/electron-browser/webviewEditorInput'; +import { IWebviewEditorService, WebviewInputOptions, WebviewReviver } from 'vs/workbench/parts/webview/electron-browser/webviewEditorService'; +import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService'; +import { extHostNamedCustomer } from './extHostCustomers'; @extHostNamedCustomer(MainContext.MainThreadWebviews) -export class MainThreadWebviews implements MainThreadWebviewsShape { +export class MainThreadWebviews implements MainThreadWebviewsShape, WebviewReviver { + + private static readonly serializeTimeout = 500; // ms + + private static readonly viewType = 'mainThreadWebview'; + private static readonly standardSupportedLinkSchemes = ['http', 'https', 'mailto']; - private _toDispose: Disposable[] = []; + private static revivalPool = 0; + + private _toDispose: IDisposable[] = []; private readonly _proxy: ExtHostWebviewsShape; - private readonly _webviews = new Map(); + private readonly _webviews = new Map(); + private readonly _revivers = new Set(); - private _activeWebview: WebviewInput | undefined = undefined; + private _activeWebview: WebviewPanelHandle | undefined = undefined; constructor( context: IExtHostContext, - @IContextKeyService _contextKeyService: IContextKeyService, - @IPartService private readonly _partService: IPartService, + @IContextKeyService contextKeyService: IContextKeyService, + @IEditorGroupService editorGroupService: IEditorGroupService, + @ILifecycleService lifecycleService: ILifecycleService, @IWorkbenchEditorService private readonly _editorService: IWorkbenchEditorService, - @IEditorGroupService private readonly _editorGroupService: IEditorGroupService, - @IOpenerService private readonly _openerService: IOpenerService + @IWebviewEditorService private readonly _webviewService: IWebviewEditorService, + @IOpenerService private readonly _openerService: IOpenerService, + @IExtensionService private readonly _extensionService: IExtensionService, + ) { this._proxy = context.getProxy(ExtHostContext.ExtHostWebviews); - _editorGroupService.onEditorsChanged(this.onEditorsChanged, this, this._toDispose); + editorGroupService.onEditorsChanged(this.onEditorsChanged, this, this._toDispose); + editorGroupService.onEditorGroupMoved(this.onEditorGroupMoved, this, this._toDispose); + + this._toDispose.push(_webviewService.registerReviver(MainThreadWebviews.viewType, this)); + + lifecycleService.onWillShutdown(e => { + e.veto(this._onWillShutdown()); + }, this, this._toDispose); } dispose(): void { this._toDispose = dispose(this._toDispose); } - $createWebview( - handle: WebviewHandle, + $createWebviewPanel( + handle: WebviewPanelHandle, viewType: string, title: string, column: Position, - options: vscode.WebviewOptions, + options: WebviewInputOptions, extensionFolderPath: string ): void { - const webviewInput = new WebviewInput(title, options, '', { - onMessage: message => this._proxy.$onMessage(handle, message), - onDidChangePosition: position => this._proxy.$onDidChangePosition(handle, position), - onDispose: () => { - this._proxy.$onDidDisposeWeview(handle).then(() => { - this._webviews.delete(handle); - }); - }, - onDidClickLink: (link, options) => this.onDidClickLink(link, options) - }, this._partService); + const webview = this._webviewService.createWebview(MainThreadWebviews.viewType, title, column, options, extensionFolderPath, this.createWebviewEventDelegate(handle)); + webview.state = { + viewType: viewType, + state: undefined + }; - this._webviews.set(handle, webviewInput); - - this._editorService.openEditor(webviewInput, { pinned: true }, column); + this._webviews.set(handle, webview); } - $disposeWebview(handle: WebviewHandle): void { + $disposeWebview(handle: WebviewPanelHandle): void { const webview = this.getWebview(handle); - if (webview) { - this._editorService.closeEditor(webview.position, webview); - } + webview.dispose(); } - $setTitle(handle: WebviewHandle, value: string): void { + $setTitle(handle: WebviewPanelHandle, value: string): void { const webview = this.getWebview(handle); webview.setName(value); } - $setHtml(handle: WebviewHandle, value: string): void { + $setHtml(handle: WebviewPanelHandle, value: string): void { const webview = this.getWebview(handle); - webview.setHtml(value); + webview.html = value; } - $reveal(handle: WebviewHandle, column: Position): void { - const webviewInput = this.getWebview(handle); - if (webviewInput.position === column) { - this._editorService.openEditor(webviewInput, { preserveFocus: true }, column); - } else { - this._editorGroupService.moveEditor(webviewInput, webviewInput.position, column, { preserveFocus: true }); - } + $reveal(handle: WebviewPanelHandle, column: Position | undefined): void { + const webview = this.getWebview(handle); + this._webviewService.revealWebview(webview, column); } - async $sendMessage(handle: WebviewHandle, message: any): Promise { - const webviewInput = this.getWebview(handle); + async $postMessage(handle: WebviewPanelHandle, message: any): TPromise { + const webview = this.getWebview(handle); const editors = this._editorService.getVisibleEditors() .filter(e => e instanceof WebviewEditor) .map(e => e as WebviewEditor) - .filter(e => e.input.matches(webviewInput)); + .filter(e => e.input.matches(webview)); for (const editor of editors) { editor.sendMessage(message); @@ -110,18 +116,87 @@ export class MainThreadWebviews implements MainThreadWebviewsShape { return (editors.length > 0); } - private getWebview(handle: number): WebviewInput { - const webviewInput = this._webviews.get(handle); - if (!webviewInput) { + $registerSerializer(viewType: string): void { + this._revivers.add(viewType); + } + + $unregisterSerializer(viewType: string): void { + this._revivers.delete(viewType); + } + + reviveWebview(webview: WebviewEditorInput): TPromise { + const viewType = webview.state.viewType; + return this._extensionService.activateByEvent(`onView:${viewType}`).then(() => { + const handle = 'revival-' + MainThreadWebviews.revivalPool++; + this._webviews.set(handle, webview); + webview._events = this.createWebviewEventDelegate(handle); + + return this._proxy.$deserializeWebviewPanel(handle, webview.state.viewType, webview.getTitle(), webview.state.state, webview.position, webview.options) + .then(undefined, () => { + webview.html = MainThreadWebviews.getDeserializationFailedContents(viewType); + }); + }); + } + + canRevive(webview: WebviewEditorInput): boolean { + return this._revivers.has(webview.viewType) || webview.reviver !== null; + } + + private _onWillShutdown(): TPromise { + const toRevive: WebviewPanelHandle[] = []; + this._webviews.forEach((view, key) => { + if (this.canRevive(view)) { + toRevive.push(key); + } + }); + + const reviveResponses = toRevive.map(handle => + TPromise.any([ + this._proxy.$serializeWebviewPanel(handle).then( + state => ({ handle, state }), + () => ({ handle, state: null })), + TPromise.timeout(MainThreadWebviews.serializeTimeout).then(() => ({ handle, state: null })) + ]).then(x => x.value)); + + return TPromise.join(reviveResponses).then(results => { + for (const result of results) { + const view = this._webviews.get(result.handle); + if (view) { + if (result.state) { + view.state.state = result.state; + } else { + view.state = null; + } + } + } + return false; // Don't veto shutdown + }); + } + + private createWebviewEventDelegate(handle: WebviewPanelHandle) { + return { + onDidClickLink: uri => this.onDidClickLink(handle, uri), + onMessage: message => this._proxy.$onMessage(handle, message), + onDispose: () => { + this._proxy.$onDidDisposeWebviewPanel(handle).then(() => { + this._webviews.delete(handle); + }); + } + }; + } + + private getWebview(handle: WebviewPanelHandle): WebviewEditorInput { + const webview = this._webviews.get(handle); + if (!webview) { throw new Error('Unknown webview handle:' + handle); } - return webviewInput; + return webview; } private onEditorsChanged() { const activeEditor = this._editorService.getActiveEditor(); - let newActiveWebview: { input: WebviewInput, handle: WebviewHandle } | undefined = undefined; - if (activeEditor && activeEditor.input instanceof WebviewInput) { + let newActiveWebview: { input: WebviewEditorInput, handle: WebviewPanelHandle } | undefined = undefined; + if (activeEditor && activeEditor.input instanceof WebviewEditorInput) { for (const handle of map.keys(this._webviews)) { const input = this._webviews.get(handle); if (input.matches(activeEditor.input)) { @@ -131,27 +206,63 @@ export class MainThreadWebviews implements MainThreadWebviewsShape { } } + if (newActiveWebview && newActiveWebview.handle === this._activeWebview) { + // No change + return; + } + + // Broadcast view state update for currently active + if (typeof this._activeWebview !== 'undefined') { + const oldActiveWebview = this._webviews.get(this._activeWebview); + if (oldActiveWebview) { + this._proxy.$onDidChangeWebviewPanelViewState(this._activeWebview, false, oldActiveWebview.position); + } + } + + // Then for newly active if (newActiveWebview) { - if (!this._activeWebview || !newActiveWebview.input.matches(this._activeWebview)) { - this._proxy.$onDidChangeActiveWeview(newActiveWebview.handle); - this._activeWebview = newActiveWebview.input; - } + this._proxy.$onDidChangeWebviewPanelViewState(newActiveWebview.handle, true, activeEditor.position); + this._activeWebview = newActiveWebview.handle; } else { - if (this._activeWebview) { - this._proxy.$onDidChangeActiveWeview(undefined); - this._activeWebview = undefined; - } + this._activeWebview = undefined; } } - private onDidClickLink(link: URI, options: vscode.WebviewOptions): void { + private onEditorGroupMoved(): void { + for (const workbenchEditor of this._editorService.getVisibleEditors()) { + if (!workbenchEditor.input) { + return; + } + + this._webviews.forEach((input, handle) => { + if (workbenchEditor.input.matches(input) && input.position !== workbenchEditor.position) { + input.updatePosition(workbenchEditor.position); + this._proxy.$onDidChangeWebviewPanelViewState(handle, handle === this._activeWebview, workbenchEditor.position); + } + }); + } + } + private onDidClickLink(handle: WebviewPanelHandle, link: URI): void { if (!link) { return; } - const enableCommandUris = options.enableCommandUris; + const webview = this.getWebview(handle); + const enableCommandUris = webview.options.enableCommandUris; if (MainThreadWebviews.standardSupportedLinkSchemes.indexOf(link.scheme) >= 0 || enableCommandUris && link.scheme === 'command') { this._openerService.open(link); } } + + private static getDeserializationFailedContents(viewType: string) { + return ` + + + + + + + ${localize('errorMessage', "An error occurred while restoring view:{0}", viewType)} + `; + } } diff --git a/src/vs/workbench/api/node/extHost.api.impl.ts b/src/vs/workbench/api/node/extHost.api.impl.ts index a2a727aae7a..688f4f3460f 100644 --- a/src/vs/workbench/api/node/extHost.api.impl.ts +++ b/src/vs/workbench/api/node/extHost.api.impl.ts @@ -54,10 +54,11 @@ import { ExtHostFileSystem } from 'vs/workbench/api/node/extHostFileSystem'; import { ExtHostDecorations } from 'vs/workbench/api/node/extHostDecorations'; import { toGlobPattern, toLanguageSelector } from 'vs/workbench/api/node/extHostTypeConverters'; import { ExtensionActivatedByAPI } from 'vs/workbench/api/node/extHostExtensionActivator'; -import { isFalsyOrEmpty } from 'vs/base/common/arrays'; import { OverviewRulerLane } from 'vs/editor/common/model'; import { ExtHostLogService } from 'vs/workbench/api/node/extHostLogService'; import { ExtHostWebviews } from 'vs/workbench/api/node/extHostWebview'; +import { ExtHostSearch } from './extHostSearch'; +import { ExtHostUrls } from './extHostUrls'; export interface IExtensionApiFactory { (extension: IExtensionDescription): typeof vscode; @@ -98,6 +99,7 @@ export function createApiFactory( const extHostHeapService = rpcProtocol.set(ExtHostContext.ExtHostHeapService, new ExtHostHeapService()); const extHostDecorations = rpcProtocol.set(ExtHostContext.ExtHostDecorations, new ExtHostDecorations(rpcProtocol)); const extHostWebviews = rpcProtocol.set(ExtHostContext.ExtHostWebviews, new ExtHostWebviews(rpcProtocol)); + const extHostUrls = rpcProtocol.set(ExtHostContext.ExtHostUrls, new ExtHostUrls(rpcProtocol)); const extHostDocumentsAndEditors = rpcProtocol.set(ExtHostContext.ExtHostDocumentsAndEditors, new ExtHostDocumentsAndEditors(rpcProtocol)); const extHostDocuments = rpcProtocol.set(ExtHostContext.ExtHostDocuments, new ExtHostDocuments(rpcProtocol, extHostDocumentsAndEditors)); const extHostDocumentContentProviders = rpcProtocol.set(ExtHostContext.ExtHostDocumentContentProviders, new ExtHostDocumentContentProvider(rpcProtocol, extHostDocumentsAndEditors, extHostLogService)); @@ -106,15 +108,16 @@ export function createApiFactory( const extHostCommands = rpcProtocol.set(ExtHostContext.ExtHostCommands, new ExtHostCommands(rpcProtocol, extHostHeapService, extHostLogService)); const extHostTreeViews = rpcProtocol.set(ExtHostContext.ExtHostTreeViews, new ExtHostTreeViews(rpcProtocol.getProxy(MainContext.MainThreadTreeViews), extHostCommands)); rpcProtocol.set(ExtHostContext.ExtHostWorkspace, extHostWorkspace); - const extHostDebugService = rpcProtocol.set(ExtHostContext.ExtHostDebugService, new ExtHostDebugService(rpcProtocol, extHostWorkspace)); + const extHostDebugService = rpcProtocol.set(ExtHostContext.ExtHostDebugService, new ExtHostDebugService(rpcProtocol, extHostWorkspace, extensionService, extHostDocumentsAndEditors, extHostConfiguration)); rpcProtocol.set(ExtHostContext.ExtHostConfiguration, extHostConfiguration); const extHostDiagnostics = rpcProtocol.set(ExtHostContext.ExtHostDiagnostics, new ExtHostDiagnostics(rpcProtocol)); - const extHostLanguageFeatures = rpcProtocol.set(ExtHostContext.ExtHostLanguageFeatures, new ExtHostLanguageFeatures(rpcProtocol, extHostDocuments, extHostCommands, extHostHeapService, extHostDiagnostics)); + const extHostLanguageFeatures = rpcProtocol.set(ExtHostContext.ExtHostLanguageFeatures, new ExtHostLanguageFeatures(rpcProtocol, null, extHostDocuments, extHostCommands, extHostHeapService, extHostDiagnostics)); const extHostFileSystem = rpcProtocol.set(ExtHostContext.ExtHostFileSystem, new ExtHostFileSystem(rpcProtocol, extHostLanguageFeatures)); const extHostFileSystemEvent = rpcProtocol.set(ExtHostContext.ExtHostFileSystemEventService, new ExtHostFileSystemEventService()); const extHostQuickOpen = rpcProtocol.set(ExtHostContext.ExtHostQuickOpen, new ExtHostQuickOpen(rpcProtocol, extHostWorkspace, extHostCommands)); - const extHostTerminalService = rpcProtocol.set(ExtHostContext.ExtHostTerminalService, new ExtHostTerminalService(rpcProtocol)); + const extHostTerminalService = rpcProtocol.set(ExtHostContext.ExtHostTerminalService, new ExtHostTerminalService(rpcProtocol, extHostConfiguration, extHostLogService)); const extHostSCM = rpcProtocol.set(ExtHostContext.ExtHostSCM, new ExtHostSCM(rpcProtocol, extHostCommands, extHostLogService)); + const extHostSearch = rpcProtocol.set(ExtHostContext.ExtHostSearch, new ExtHostSearch(rpcProtocol)); const extHostTask = rpcProtocol.set(ExtHostContext.ExtHostTask, new ExtHostTask(rpcProtocol, extHostWorkspace)); const extHostWindow = rpcProtocol.set(ExtHostContext.ExtHostWindow, new ExtHostWindow(rpcProtocol)); rpcProtocol.set(ExtHostContext.ExtHostExtensionService, extensionService); @@ -136,27 +139,30 @@ export function createApiFactory( return function (extension: IExtensionDescription): typeof vscode { - if (!isFalsyOrEmpty(product.extensionAllowedProposedApi) - && product.extensionAllowedProposedApi.indexOf(extension.id) >= 0 - ) { - // fast lane -> proposed api is available to all extensions - // that are listed in product.json-files - extension.enableProposedApi = true; - - } else if (extension.enableProposedApi && !extension.isBuiltin) { - if ( - !initData.environment.enableProposedApiForAll && - initData.environment.enableProposedApiFor.indexOf(extension.id) < 0 - ) { - extension.enableProposedApi = false; - console.error(`Extension '${extension.id} cannot use PROPOSED API (must started out of dev or enabled via --enable-proposed-api)`); - - } else { - // proposed api is available when developing or when an extension was explicitly - // spelled out via a command line argument - console.warn(`Extension '${extension.id}' uses PROPOSED API which is subject to change and removal without notice.`); + // Check document selectors for being overly generic. Technically this isn't a problem but + // in practice many extensions say they support `fooLang` but need fs-access to do so. Those + // extension should specify then the `file`-scheme, e.g `{ scheme: 'fooLang', language: 'fooLang' }` + // We only inform once, it is not a warning because we just want to raise awareness and because + // we cannot say if the extension is doing it right or wrong... + let checkSelector = (function () { + let done = initData.environment.extensionDevelopmentPath !== extension.extensionFolderPath; + function inform(selector: vscode.DocumentSelector) { + console.info(`Extension '${extension.id}' uses a document selector without scheme. Learn more about this: https://go.microsoft.com/fwlink/?linkid=872305`); + done = true; } - } + return function perform(selector: vscode.DocumentSelector): vscode.DocumentSelector { + if (!done) { + if (Array.isArray(selector)) { + selector.forEach(perform); + } else if (typeof selector === 'string') { + inform(selector); + } else if (typeof selector.scheme === 'undefined') { + inform(selector); + } + } + return selector; + }; + })(); // namespace: commands const commands: typeof vscode.commands = { @@ -237,72 +243,72 @@ export function createApiFactory( checkProposedApiEnabled(extension); return extHostDiagnostics.onDidChangeDiagnostics; }, - getDiagnostics: proposedApiFunction(extension, (resource?) => { - return extHostDiagnostics.getDiagnostics(resource); - }), + getDiagnostics: (resource?) => { + return extHostDiagnostics.getDiagnostics(resource); + }, getLanguages(): TPromise { return extHostLanguages.getLanguages(); }, match(selector: vscode.DocumentSelector, document: vscode.TextDocument): number { return score(toLanguageSelector(selector), document.uri, document.languageId, true); }, - registerCodeActionsProvider(selector: vscode.DocumentSelector, provider: vscode.CodeActionProvider): vscode.Disposable { - return extHostLanguageFeatures.registerCodeActionProvider(selector, provider); + registerCodeActionsProvider(selector: vscode.DocumentSelector, provider: vscode.CodeActionProvider, metadata?: vscode.CodeActionProviderMetadata): vscode.Disposable { + return extHostLanguageFeatures.registerCodeActionProvider(checkSelector(selector), provider, metadata); }, registerCodeLensProvider(selector: vscode.DocumentSelector, provider: vscode.CodeLensProvider): vscode.Disposable { - return extHostLanguageFeatures.registerCodeLensProvider(selector, provider); + return extHostLanguageFeatures.registerCodeLensProvider(checkSelector(selector), provider); }, registerDefinitionProvider(selector: vscode.DocumentSelector, provider: vscode.DefinitionProvider): vscode.Disposable { - return extHostLanguageFeatures.registerDefinitionProvider(selector, provider); + return extHostLanguageFeatures.registerDefinitionProvider(checkSelector(selector), provider); }, registerImplementationProvider(selector: vscode.DocumentSelector, provider: vscode.ImplementationProvider): vscode.Disposable { - return extHostLanguageFeatures.registerImplementationProvider(selector, provider); + return extHostLanguageFeatures.registerImplementationProvider(checkSelector(selector), provider); }, registerTypeDefinitionProvider(selector: vscode.DocumentSelector, provider: vscode.TypeDefinitionProvider): vscode.Disposable { - return extHostLanguageFeatures.registerTypeDefinitionProvider(selector, provider); + return extHostLanguageFeatures.registerTypeDefinitionProvider(checkSelector(selector), provider); }, registerHoverProvider(selector: vscode.DocumentSelector, provider: vscode.HoverProvider): vscode.Disposable { - return extHostLanguageFeatures.registerHoverProvider(selector, provider, extension.id); + return extHostLanguageFeatures.registerHoverProvider(checkSelector(selector), provider, extension.id); }, registerDocumentHighlightProvider(selector: vscode.DocumentSelector, provider: vscode.DocumentHighlightProvider): vscode.Disposable { - return extHostLanguageFeatures.registerDocumentHighlightProvider(selector, provider); + return extHostLanguageFeatures.registerDocumentHighlightProvider(checkSelector(selector), provider); }, registerReferenceProvider(selector: vscode.DocumentSelector, provider: vscode.ReferenceProvider): vscode.Disposable { - return extHostLanguageFeatures.registerReferenceProvider(selector, provider); + return extHostLanguageFeatures.registerReferenceProvider(checkSelector(selector), provider); }, registerRenameProvider(selector: vscode.DocumentSelector, provider: vscode.RenameProvider): vscode.Disposable { - return extHostLanguageFeatures.registerRenameProvider(selector, provider, extension.enableProposedApi); + return extHostLanguageFeatures.registerRenameProvider(checkSelector(selector), provider, extension.enableProposedApi); }, registerDocumentSymbolProvider(selector: vscode.DocumentSelector, provider: vscode.DocumentSymbolProvider): vscode.Disposable { - return extHostLanguageFeatures.registerDocumentSymbolProvider(selector, provider); + return extHostLanguageFeatures.registerDocumentSymbolProvider(checkSelector(selector), provider); }, registerWorkspaceSymbolProvider(provider: vscode.WorkspaceSymbolProvider): vscode.Disposable { return extHostLanguageFeatures.registerWorkspaceSymbolProvider(provider); }, registerDocumentFormattingEditProvider(selector: vscode.DocumentSelector, provider: vscode.DocumentFormattingEditProvider): vscode.Disposable { - return extHostLanguageFeatures.registerDocumentFormattingEditProvider(selector, provider); + return extHostLanguageFeatures.registerDocumentFormattingEditProvider(checkSelector(selector), provider); }, registerDocumentRangeFormattingEditProvider(selector: vscode.DocumentSelector, provider: vscode.DocumentRangeFormattingEditProvider): vscode.Disposable { - return extHostLanguageFeatures.registerDocumentRangeFormattingEditProvider(selector, provider); + return extHostLanguageFeatures.registerDocumentRangeFormattingEditProvider(checkSelector(selector), provider); }, registerOnTypeFormattingEditProvider(selector: vscode.DocumentSelector, provider: vscode.OnTypeFormattingEditProvider, firstTriggerCharacter: string, ...moreTriggerCharacters: string[]): vscode.Disposable { - return extHostLanguageFeatures.registerOnTypeFormattingEditProvider(selector, provider, [firstTriggerCharacter].concat(moreTriggerCharacters)); + return extHostLanguageFeatures.registerOnTypeFormattingEditProvider(checkSelector(selector), provider, [firstTriggerCharacter].concat(moreTriggerCharacters)); }, registerSignatureHelpProvider(selector: vscode.DocumentSelector, provider: vscode.SignatureHelpProvider, ...triggerCharacters: string[]): vscode.Disposable { - return extHostLanguageFeatures.registerSignatureHelpProvider(selector, provider, triggerCharacters); + return extHostLanguageFeatures.registerSignatureHelpProvider(checkSelector(selector), provider, triggerCharacters); }, registerCompletionItemProvider(selector: vscode.DocumentSelector, provider: vscode.CompletionItemProvider, ...triggerCharacters: string[]): vscode.Disposable { - return extHostLanguageFeatures.registerCompletionItemProvider(selector, provider, triggerCharacters); + return extHostLanguageFeatures.registerCompletionItemProvider(checkSelector(selector), provider, triggerCharacters); }, registerDocumentLinkProvider(selector: vscode.DocumentSelector, provider: vscode.DocumentLinkProvider): vscode.Disposable { - return extHostLanguageFeatures.registerDocumentLinkProvider(selector, provider); + return extHostLanguageFeatures.registerDocumentLinkProvider(checkSelector(selector), provider); }, registerColorProvider(selector: vscode.DocumentSelector, provider: vscode.DocumentColorProvider): vscode.Disposable { - return extHostLanguageFeatures.registerColorProvider(selector, provider); + return extHostLanguageFeatures.registerColorProvider(checkSelector(selector), provider); + }, + registerFoldingRangeProvider(selector: vscode.DocumentSelector, provider: vscode.FoldingRangeProvider): vscode.Disposable { + return extHostLanguageFeatures.registerFoldingRangeProvider(checkSelector(selector), provider); }, - registerFoldingProvider: proposedApiFunction(extension, (selector: vscode.DocumentSelector, provider: vscode.FoldingProvider): vscode.Disposable => { - return extHostLanguageFeatures.registerFoldingProvider(selector, provider); - }), setLanguageConfiguration: (language: string, configuration: vscode.LanguageConfiguration): vscode.Disposable => { return extHostLanguageFeatures.setLanguageConfiguration(language, configuration); } @@ -316,6 +322,9 @@ export function createApiFactory( get visibleTextEditors() { return extHostEditors.getVisibleTextEditors(); }, + get terminals() { + return proposedApiFunction(extension, extHostTerminalService.terminals); + }, showTextDocument(documentOrUri: vscode.TextDocument | vscode.Uri, columnOrOptions?: vscode.ViewColumn | vscode.TextDocumentShowOptions, preserveFocus?: boolean): TPromise { let documentPromise: TPromise; if (URI.isUri(documentOrUri)) { @@ -342,15 +351,18 @@ export function createApiFactory( onDidChangeTextEditorOptions(listener: (e: vscode.TextEditorOptionsChangeEvent) => any, thisArgs?: any, disposables?: extHostTypes.Disposable[]) { return extHostEditors.onDidChangeTextEditorOptions(listener, thisArgs, disposables); }, - onDidChangeTextEditorVisibleRanges: proposedApiFunction(extension, (listener: (e: vscode.TextEditorVisibleRangesChangeEvent) => any, thisArgs?: any, disposables?: extHostTypes.Disposable[]) => { + onDidChangeTextEditorVisibleRanges(listener: (e: vscode.TextEditorVisibleRangesChangeEvent) => any, thisArgs?: any, disposables?: extHostTypes.Disposable[]) { return extHostEditors.onDidChangeTextEditorVisibleRanges(listener, thisArgs, disposables); - }), + }, onDidChangeTextEditorViewColumn(listener, thisArg?, disposables?) { return extHostEditors.onDidChangeTextEditorViewColumn(listener, thisArg, disposables); }, onDidCloseTerminal(listener, thisArg?, disposables?) { return extHostTerminalService.onDidCloseTerminal(listener, thisArg, disposables); }, + onDidOpenTerminal: proposedApiFunction(extension, (listener, thisArg?, disposables?) => { + return extHostTerminalService.onDidOpenTerminal(listener, thisArg, disposables); + }), get state() { return extHostWindow.state; }, @@ -366,7 +378,7 @@ export function createApiFactory( showErrorMessage(message, first, ...rest) { return extHostMessageService.showMessage(extension, Severity.Error, message, first, rest); }, - showQuickPick(items: any, options: vscode.QuickPickOptions, token?: vscode.CancellationToken) { + showQuickPick(items: any, options: vscode.QuickPickOptions, token?: vscode.CancellationToken): any { return extHostQuickOpen.showQuickPick(items, options, token); }, showWorkspaceFolderPick(options: vscode.WorkspaceFolderPickOptions) { @@ -397,6 +409,9 @@ export function createApiFactory( createOutputChannel(name: string): vscode.OutputChannel { return extHostOutputService.createOutputChannel(name); }, + createWebviewPanel(viewType: string, title: string, column: vscode.ViewColumn, options: vscode.WebviewPanelOptions & vscode.WebviewOptions): vscode.WebviewPanel { + return extHostWebviews.createWebview(viewType, title, column, options, extension.extensionFolderPath); + }, createTerminal(nameOrOptions: vscode.TerminalOptions | string, shellPath?: string, shellArgs?: string[]): vscode.Terminal { if (typeof nameOrOptions === 'object') { return extHostTerminalService.createTerminalFromOptions(nameOrOptions); @@ -416,8 +431,11 @@ export function createApiFactory( registerDecorationProvider: proposedApiFunction(extension, (provider: vscode.DecorationProvider) => { return extHostDecorations.registerDecorationProvider(provider, extension.id); }), - createWebview: proposedApiFunction(extension, (viewType: string, title: string, column: vscode.ViewColumn, options: vscode.WebviewOptions) => { - return extHostWebviews.createWebview(viewType, title, column, options, extension.extensionFolderPath); + registerWebviewPanelSerializer: proposedApiFunction(extension, (viewType: string, serializer: vscode.WebviewPanelSerializer) => { + return extHostWebviews.registerWebviewPanelSerializer(viewType, serializer); + }), + registerExternalUriHandler: proposedApiFunction(extension, (handler: vscode.ExternalUriHandler) => { + return extHostUrls.registerExternalUriHandler(extension.id, handler); }) }; @@ -517,26 +535,32 @@ export function createApiFactory( registerTaskProvider: (type: string, provider: vscode.TaskProvider) => { return extHostTask.registerTaskProvider(extension, provider); }, - fetchTasks: proposedApiFunction(extension, (): Thenable => { - return extHostTask.executeTaskProvider(); + fetchTasks: proposedApiFunction(extension, (filter?: vscode.TaskFilter): Thenable => { + return extHostTask.fetchTasks(filter); }), executeTask: proposedApiFunction(extension, (task: vscode.Task): Thenable => { return extHostTask.executeTask(extension, task); }), + get taskExecutions(): vscode.TaskExecution[] { + return extHostTask.taskExecutions; + }, onDidStartTask: (listeners, thisArgs?, disposables?) => { return extHostTask.onDidStartTask(listeners, thisArgs, disposables); }, - terminateTask: proposedApiFunction(extension, (task: vscode.TaskExecution): void => { - extHostTask.terminateTask(task); - }), onDidEndTask: (listeners, thisArgs?, disposables?) => { return extHostTask.onDidEndTask(listeners, thisArgs, disposables); }, - registerFileSystemProvider: proposedApiFunction(extension, (scheme, provider) => { - return extHostFileSystem.registerFileSystemProvider(scheme, provider); + registerFileSystemProvider: proposedApiFunction(extension, (scheme, provider, newProvider?) => { + return extHostFileSystem.registerFileSystemProvider(scheme, provider, newProvider); + }), + registerFileSystemProvider2: proposedApiFunction(extension, (scheme, provider, options) => { + return extHostFileSystem.registerFileSystemProvider2(scheme, provider, options); + }), + registerDeprecatedFileSystemProvider: proposedApiFunction(extension, (scheme, provider) => { + return extHostFileSystem.registerDeprecatedFileSystemProvider(scheme, provider); }), registerSearchProvider: proposedApiFunction(extension, (scheme, provider) => { - return extHostFileSystem.registerSearchProvider(scheme, provider); + return extHostSearch.registerSearchProvider(scheme, provider); }) }; @@ -673,9 +697,12 @@ export function createApiFactory( FileChangeType: extHostTypes.FileChangeType, FileType: extHostTypes.FileType, - FoldingRangeList: extHostTypes.FoldingRangeList, + DeprecatedFileChangeType: extHostTypes.FileChangeType, + DeprecatedFileType: extHostTypes.FileType, + FileChangeType2: extHostTypes.FileChangeType2, + FileSystemError: extHostTypes.FileSystemError, FoldingRange: extHostTypes.FoldingRange, - FoldingRangeType: extHostTypes.FoldingRangeType + FoldingRangeKind: extHostTypes.FoldingRangeKind }; }; } @@ -704,7 +731,7 @@ class Extension implements vscode.Extension { } activate(): Thenable { - return this._extensionService.activateById(this.id, new ExtensionActivatedByAPI(false)).then(() => this.exports); + return this._extensionService.activateByIdWithErrors(this.id, new ExtensionActivatedByAPI(false)).then(() => this.exports); } } diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index 6e33c3e8955..a31a36c4c7c 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -24,7 +24,7 @@ import * as editorCommon from 'vs/editor/common/editorCommon'; import * as modes from 'vs/editor/common/modes'; import { IConfigurationData, ConfigurationTarget, IConfigurationModel } from 'vs/platform/configuration/common/configuration'; -import { IConfig, IAdapterExecutable } from 'vs/workbench/parts/debug/common/debug'; +import { IConfig, IAdapterExecutable, ITerminalSettings } from 'vs/workbench/parts/debug/common/debug'; import { IPickOpenEntry, IPickOptions } from 'vs/platform/quickOpen/common/quickOpen'; import { SaveReason } from 'vs/workbench/services/textfile/common/textfiles'; @@ -42,18 +42,16 @@ import { ITreeItem } from 'vs/workbench/common/views'; import { ThemeColor } from 'vs/platform/theme/common/themeService'; import { IDisposable } from 'vs/base/common/lifecycle'; import { SerializedError } from 'vs/base/common/errors'; -import { IStat, FileChangeType } from 'vs/platform/files/common/files'; +import { IStat, FileChangeType, IWatchOptions, FileSystemProviderCapabilities, FileOptions } from 'vs/platform/files/common/files'; import { ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; import { CommentRule, CharacterPair, EnterAction } from 'vs/editor/common/modes/languageConfiguration'; import { ISingleEditOperation } from 'vs/editor/common/model'; import { ILineMatch, IPatternInfo } from 'vs/platform/search/common/search'; import { LogLevel } from 'vs/platform/log/common/log'; -import { TaskExecutionDTO, TaskDTO, TaskHandleDTO } from 'vs/workbench/api/shared/tasks'; +import { TaskExecutionDTO, TaskDTO, TaskHandleDTO, TaskFilterDTO } from 'vs/workbench/api/shared/tasks'; export interface IEnvironment { isExtensionDevelopmentDebug: boolean; - enableProposedApiForAll: boolean; - enableProposedApiFor: string | string[]; appRoot: string; appSettingsHome: string; disableExtensions: boolean; @@ -273,7 +271,7 @@ export interface MainThreadLanguageFeaturesShape extends IDisposable { $registerHoverProvider(handle: number, selector: ISerializedDocumentFilter[]): void; $registerDocumentHighlightProvider(handle: number, selector: ISerializedDocumentFilter[]): void; $registerReferenceSupport(handle: number, selector: ISerializedDocumentFilter[]): void; - $registerQuickFixSupport(handle: number, selector: ISerializedDocumentFilter[]): void; + $registerQuickFixSupport(handle: number, selector: ISerializedDocumentFilter[], supportedKinds?: string[]): void; $registerDocumentFormattingSupport(handle: number, selector: ISerializedDocumentFilter[]): void; $registerRangeFormattingSupport(handle: number, selector: ISerializedDocumentFilter[]): void; $registerOnTypeFormattingSupport(handle: number, selector: ISerializedDocumentFilter[], autoFormatTriggerCharacters: string[]): void; @@ -283,7 +281,7 @@ export interface MainThreadLanguageFeaturesShape extends IDisposable { $registerSignatureHelpProvider(handle: number, selector: ISerializedDocumentFilter[], triggerCharacter: string[]): void; $registerDocumentLinkProvider(handle: number, selector: ISerializedDocumentFilter[]): void; $registerDocumentColorProvider(handle: number, selector: ISerializedDocumentFilter[]): void; - $registerFoldingProvider(handle: number, selector: ISerializedDocumentFilter[]): void; + $registerFoldingRangeProvider(handle: number, selector: ISerializedDocumentFilter[]): void; $setLanguageConfiguration(handle: number, languageId: string, configuration: ISerializedLanguageConfiguration): void; } @@ -321,13 +319,18 @@ export interface MainThreadTerminalServiceShape extends IDisposable { $hide(terminalId: number): void; $sendText(terminalId: number, text: string, addNewLine: boolean): void; $show(terminalId: number, preserveFocus: boolean): void; + + $sendProcessTitle(terminalId: number, title: string): void; + $sendProcessData(terminalId: number, data: string): void; + $sendProcessPid(terminalId: number, pid: number): void; + $sendProcessExit(terminalId: number, exitCode: number): void; } export interface MyQuickPickItems extends IPickOpenEntry { handle: number; } export interface MainThreadQuickOpenShape extends IDisposable { - $show(options: IPickOptions): TPromise; + $show(options: IPickOptions): TPromise; $setItems(items: MyQuickPickItems[]): TPromise; $setError(error: Error): TPromise; $input(options: vscode.InputBoxOptions, validateInput: boolean): TPromise; @@ -347,21 +350,35 @@ export interface MainThreadTelemetryShape extends IDisposable { $publicLog(eventName: string, data?: any): void; } -export type WebviewHandle = number; +export type WebviewPanelHandle = string; export interface MainThreadWebviewsShape extends IDisposable { - $createWebview(handle: WebviewHandle, viewType: string, title: string, column: EditorPosition, options: vscode.WebviewOptions, extensionFolderPath: string): void; - $disposeWebview(handle: WebviewHandle): void; - $reveal(handle: WebviewHandle, column: EditorPosition): void; - $setTitle(handle: WebviewHandle, value: string): void; - $setHtml(handle: WebviewHandle, value: string): void; - $sendMessage(handle: WebviewHandle, value: any): Thenable; + $createWebviewPanel(handle: WebviewPanelHandle, viewType: string, title: string, column: EditorPosition, options: vscode.WebviewPanelOptions & vscode.WebviewOptions, extensionFolderPath: string): void; + $disposeWebview(handle: WebviewPanelHandle): void; + $reveal(handle: WebviewPanelHandle, column: EditorPosition | undefined): void; + $setTitle(handle: WebviewPanelHandle, value: string): void; + $setHtml(handle: WebviewPanelHandle, value: string): void; + $postMessage(handle: WebviewPanelHandle, value: any): Thenable; + + $registerSerializer(viewType: string): void; + $unregisterSerializer(viewType: string): void; } + export interface ExtHostWebviewsShape { - $onMessage(handle: WebviewHandle, message: any): void; - $onDidChangeActiveWeview(handle: WebviewHandle | undefined): void; - $onDidDisposeWeview(handle: WebviewHandle): Thenable; - $onDidChangePosition(handle: WebviewHandle, newPosition: EditorPosition): void; + $onMessage(handle: WebviewPanelHandle, message: any): void; + $onDidChangeWebviewPanelViewState(handle: WebviewPanelHandle, active: boolean, position: EditorPosition): void; + $onDidDisposeWebviewPanel(handle: WebviewPanelHandle): Thenable; + $deserializeWebviewPanel(newWebviewHandle: WebviewPanelHandle, viewType: string, title: string, state: any, position: EditorPosition, options: vscode.WebviewOptions): Thenable; + $serializeWebviewPanel(webviewHandle: WebviewPanelHandle): Thenable; +} + +export interface MainThreadUrlsShape extends IDisposable { + $registerExternalUriHandler(handle: number, extensionId: string): TPromise; + $unregisterExternalUriHandler(handle: number): TPromise; +} + +export interface ExtHostUrlsShape { + $handleExternalUri(handle: number, uri: UriComponents): TPromise; } export interface MainThreadWorkspaceShape extends IDisposable { @@ -377,21 +394,22 @@ export interface IFileChangeDto { } export interface MainThreadFileSystemShape extends IDisposable { - $registerFileSystemProvider(handle: number, scheme: string): void; + $registerFileSystemProvider(handle: number, scheme: string, capabilities: FileSystemProviderCapabilities): void; + $unregisterProvider(handle: number): void; + $onFileSystemChange(handle: number, resource: IFileChangeDto[]): void; +} + +export interface MainThreadSearchShape extends IDisposable { $registerSearchProvider(handle: number, scheme: string): void; $unregisterProvider(handle: number): void; - - $onFileSystemChange(handle: number, resource: IFileChangeDto[]): void; - $reportFileChunk(handle: number, session: number, chunk: number[] | null): void; - $handleFindMatch(handle: number, session, data: UriComponents | [UriComponents, ILineMatch]): void; } export interface MainThreadTaskShape extends IDisposable { $registerTaskProvider(handle: number): TPromise; - $executeTaskProvider(): TPromise; + $fetchTasks(filter?: TaskFilterDTO): TPromise; $executeTask(task: TaskHandleDTO | TaskDTO): TPromise; - $terminateTask(task: TaskExecutionDTO): TPromise; + $terminateTask(id: string): TPromise; $unregisterTaskProvider(handle: number): TPromise; } @@ -459,6 +477,10 @@ export interface MainThreadSCMShape extends IDisposable { export type DebugSessionUUID = string; export interface MainThreadDebugServiceShape extends IDisposable { + $registerDebugTypes(debugTypes: string[]); + $acceptDAMessage(handle: number, message: DebugProtocol.ProtocolMessage); + $acceptDAError(handle: number, name: string, message: string, stack: string); + $acceptDAExit(handle: number, code: number, signal: string); $registerDebugConfigurationProvider(type: string, hasProvideMethod: boolean, hasResolveMethod: boolean, hasDebugAdapterExecutable: boolean, handle: number): TPromise; $unregisterDebugConfigurationProvider(handle: number): TPromise; $startDebugging(folder: UriComponents | undefined, nameOrConfig: string | vscode.DebugConfiguration): TPromise; @@ -558,15 +580,19 @@ export interface ExtHostWorkspaceShape { } export interface ExtHostFileSystemShape { - $utimes(handle: number, resource: UriComponents, mtime: number, atime: number): TPromise; $stat(handle: number, resource: UriComponents): TPromise; - $read(handle: number, session: number, offset: number, count: number, resource: UriComponents): TPromise; - $write(handle: number, resource: UriComponents, content: number[]): TPromise; - $unlink(handle: number, resource: UriComponents): TPromise; - $move(handle: number, resource: UriComponents, target: UriComponents): TPromise; + $readFile(handle: number, resource: UriComponents, opts: FileOptions): TPromise; + $writeFile(handle: number, resource: UriComponents, base64Encoded: string, opts: FileOptions): TPromise; + $rename(handle: number, resource: UriComponents, target: UriComponents, opts: FileOptions): TPromise; + $copy(handle: number, resource: UriComponents, target: UriComponents, opts: FileOptions): TPromise; $mkdir(handle: number, resource: UriComponents): TPromise; - $readdir(handle: number, resource: UriComponents): TPromise<[UriComponents, IStat][]>; - $rmdir(handle: number, resource: UriComponents): TPromise; + $readdir(handle: number, resource: UriComponents): TPromise<[string, IStat][]>; + $delete(handle: number, resource: UriComponents): TPromise; + $watch(handle: number, session: number, resource: UriComponents, opts: IWatchOptions): void; + $unwatch(handle: number, session: number): void; +} + +export interface ExtHostSearchShape { $provideFileSearchResults(handle: number, session: number, query: string): TPromise; $provideTextSearchResults(handle: number, session: number, pattern: IPatternInfo, options: { includes: string[], excludes: string[] }): TPromise; } @@ -700,7 +726,7 @@ export interface ExtHostLanguageFeaturesShape { $resolveWorkspaceSymbol(handle: number, symbol: SymbolInformationDto): TPromise; $releaseWorkspaceSymbols(handle: number, id: number): void; $provideRenameEdits(handle: number, resource: UriComponents, position: IPosition, newName: string): TPromise; - $resolveRenameLocation(handle: number, resource: UriComponents, position: IPosition): TPromise; + $resolveRenameLocation(handle: number, resource: UriComponents, position: IPosition): TPromise; $provideCompletionItems(handle: number, resource: UriComponents, position: IPosition, context: modes.SuggestContext): TPromise; $resolveCompletionItem(handle: number, resource: UriComponents, position: IPosition, suggestion: modes.ISuggestion): TPromise; $releaseCompletionItems(handle: number, id: number): void; @@ -709,7 +735,7 @@ export interface ExtHostLanguageFeaturesShape { $resolveDocumentLink(handle: number, link: modes.ILink): TPromise; $provideDocumentColors(handle: number, resource: UriComponents): TPromise; $provideColorPresentations(handle: number, resource: UriComponents, colorInfo: IRawColorInfo): TPromise; - $provideFoldingRanges(handle: number, resource: UriComponents, context: modes.FoldingContext): TPromise; + $provideFoldingRanges(handle: number, resource: UriComponents, context: modes.FoldingContext): TPromise; } export interface ExtHostQuickOpenShape { @@ -717,9 +743,22 @@ export interface ExtHostQuickOpenShape { $validateInput(input: string): TPromise; } +export interface ShellLaunchConfigDto { + name?: string; + executable?: string; + args?: string[] | string; + cwd?: string; + env?: { [key: string]: string }; +} + export interface ExtHostTerminalServiceShape { $acceptTerminalClosed(id: number): void; + $acceptTerminalOpened(id: number, name: string): void; $acceptTerminalProcessId(id: number, processId: number): void; + $createProcess(id: number, shellLaunchConfig: ShellLaunchConfigDto, cols: number, rows: number): void; + $acceptProcessInput(id: number, data: string): void; + $acceptProcessResize(id: number, cols: number, rows: number): void; + $acceptProcessShutdown(id: number): void; } export interface ExtHostSCMShape { @@ -777,6 +816,11 @@ export interface ISourceMultiBreakpointDto { } export interface ExtHostDebugServiceShape { + $substituteVariables(folder: UriComponents | undefined, config: IConfig): TPromise; + $runInTerminal(args: DebugProtocol.RunInTerminalRequestArguments, config: ITerminalSettings): TPromise; + $startDASession(handle: number, debugType: string, adapterExecutableInfo: IAdapterExecutable | null): TPromise; + $stopDASession(handle: number): TPromise; + $sendDAMessage(handle: number, message: DebugProtocol.ProtocolMessage): TPromise; $resolveDebugConfiguration(handle: number, folder: UriComponents | undefined, debugConfiguration: IConfig): TPromise; $provideDebugConfigurations(handle: number, folder: UriComponents | undefined): TPromise; $debugAdapterExecutable(handle: number, folder: UriComponents | undefined): TPromise; @@ -838,10 +882,12 @@ export const MainContext = { MainThreadTelemetry: createMainId('MainThreadTelemetry'), MainThreadTerminalService: createMainId('MainThreadTerminalService'), MainThreadWebviews: createMainId('MainThreadWebviews'), + MainThreadUrls: createMainId('MainThreadUrls'), MainThreadWorkspace: createMainId('MainThreadWorkspace'), MainThreadFileSystem: createMainId('MainThreadFileSystem'), MainThreadExtensionService: createMainId('MainThreadExtensionService'), MainThreadSCM: createMainId('MainThreadSCM'), + MainThreadSearch: createMainId('MainThreadSearch'), MainThreadTask: createMainId('MainThreadTask'), MainThreadWindow: createMainId('MainThreadWindow'), }; @@ -867,9 +913,11 @@ export const ExtHostContext = { ExtHostLogService: createExtId('ExtHostLogService'), ExtHostTerminalService: createExtId('ExtHostTerminalService'), ExtHostSCM: createExtId('ExtHostSCM'), + ExtHostSearch: createExtId('ExtHostSearch'), ExtHostTask: createExtId('ExtHostTask'), ExtHostWorkspace: createExtId('ExtHostWorkspace'), ExtHostWindow: createExtId('ExtHostWindow'), ExtHostWebviews: createExtId('ExtHostWebviews'), + ExtHostUrls: createExtId('ExtHostUrls'), ExtHostProgress: createMainId('ExtHostProgress') }; diff --git a/src/vs/workbench/api/node/extHostApiCommands.ts b/src/vs/workbench/api/node/extHostApiCommands.ts index 3122072f43c..4fdd04533c5 100644 --- a/src/vs/workbench/api/node/extHostApiCommands.ts +++ b/src/vs/workbench/api/node/extHostApiCommands.ts @@ -10,6 +10,8 @@ import { IDisposable } from 'vs/base/common/lifecycle'; import * as vscode from 'vscode'; import * as typeConverters from 'vs/workbench/api/node/extHostTypeConverters'; import * as types from 'vs/workbench/api/node/extHostTypes'; +import { IRawColorInfo } from 'vs/workbench/api/node/extHost.protocol'; + import { ISingleEditOperation } from 'vs/editor/common/model'; import * as modes from 'vs/editor/common/modes'; import { ICommandHandlerDescription } from 'vs/platform/commands/common/commands'; @@ -119,7 +121,8 @@ export class ExtHostApiCommands { args: [ { name: 'uri', description: 'Uri of a text document', constraint: URI }, { name: 'position', description: 'Position in a text document', constraint: types.Position }, - { name: 'triggerCharacter', description: '(optional) Trigger completion when the user types the character, like `,` or `(`', constraint: value => value === void 0 || typeof value === 'string' } + { name: 'triggerCharacter', description: '(optional) Trigger completion when the user types the character, like `,` or `(`', constraint: value => value === void 0 || typeof value === 'string' }, + { name: 'itemResolveCount', description: '(optional) Number of completions to resolve (too large numbers slow down completions)', constraint: value => value === void 0 || typeof value === 'number' } ], returns: 'A promise that resolves to a CompletionList-instance.' }); @@ -134,7 +137,8 @@ export class ExtHostApiCommands { this._register('vscode.executeCodeLensProvider', this._executeCodeLensProvider, { description: 'Execute CodeLens provider.', args: [ - { name: 'uri', description: 'Uri of a text document', constraint: URI } + { name: 'uri', description: 'Uri of a text document', constraint: URI }, + { name: 'itemResolveCount', description: '(optional) Number of lenses that should be resolved and returned. Will only retrun resolved lenses, will impact performance)', constraint: value => value === void 0 || typeof value === 'number' } ], returns: 'A promise that resolves to an array of CodeLens-instances.' }); @@ -177,7 +181,21 @@ export class ExtHostApiCommands { args: [], returns: 'An array of task handles' }); - + this._register('vscode.executeDocumentColorProvider', this._executeDocumentColorProvider, { + description: 'Execute document color provider.', + args: [ + { name: 'uri', description: 'Uri of a text document', constraint: URI }, + ], + returns: 'A promise that resolves to an array of ColorInformation objects.' + }); + this._register('vscode.executeColorPresentationProvider', this._executeColorPresentationProvider, { + description: 'Execute color presentation provider.', + args: [ + { name: 'color', description: 'The color to show and insert', constraint: types.Color }, + { name: 'context', description: 'Context object with uri and range' } + ], + returns: 'A promise that resolves to an array of ColorPresentation objects.' + }); this._register('vscode.previewHtml', (uri: URI, position?: vscode.ViewColumn, label?: string, options?: any) => { return this._commands.executeCommand('_workbench.previewHtml', uri, @@ -186,9 +204,9 @@ export class ExtHostApiCommands { options); }, { description: ` - Render the html of the resource in an editor view. + Render the HTML of the resource in an editor view. - See [working with the html preview](https://code.visualstudio.com/docs/extensionAPI/vscode-api-commands#working-with-the-html-preview) for more information about the html preview's intergration with the editor and for best practices for extension authors. + See [working with the HTML preview](https://code.visualstudio.com/docs/extensionAPI/vscode-api-commands#working-with-the-html-preview) for more information about the HTML preview's integration with the editor and for best practices for extension authors. `, args: [ { name: 'uri', description: 'Uri of the resource to preview.', constraint: value => value instanceof URI || typeof value === 'string' }, @@ -376,11 +394,12 @@ export class ExtHostApiCommands { }); } - private _executeCompletionItemProvider(resource: URI, position: types.Position, triggerCharacter: string): Thenable { + private _executeCompletionItemProvider(resource: URI, position: types.Position, triggerCharacter: string, maxItemsToResolve: number): Thenable { const args = { resource, position: position && typeConverters.fromPosition(position), - triggerCharacter + triggerCharacter, + maxItemsToResolve }; return this._commands.executeCommand('_executeCompletionItemProvider', args).then(result => { if (result) { @@ -391,6 +410,32 @@ export class ExtHostApiCommands { }); } + private _executeDocumentColorProvider(resource: URI): Thenable { + const args = { + resource + }; + return this._commands.executeCommand('_executeDocumentColorProvider', args).then(result => { + if (result) { + return result.map(ci => ({ range: typeConverters.toRange(ci.range), color: typeConverters.Color.to(ci.color) })); + } + return []; + }); + } + + private _executeColorPresentationProvider(color: types.Color, context: { uri: URI, range: types.Range }): Thenable { + const args = { + resource: context.uri, + color: typeConverters.Color.from(color), + range: typeConverters.fromRange(context.range), + }; + return this._commands.executeCommand('_executeColorPresentationProvider', args).then(result => { + if (result) { + return result.map(typeConverters.ColorPresentation.to); + } + return []; + }); + } + private _executeDocumentSymbolProvider(resource: URI): Thenable { const args = { resource @@ -428,8 +473,8 @@ export class ExtHostApiCommands { })); } - private _executeCodeLensProvider(resource: URI): Thenable { - const args = { resource }; + private _executeCodeLensProvider(resource: URI, itemResolveCount: number): Thenable { + const args = { resource, itemResolveCount }; return this._commands.executeCommand('_executeCodeLensProvider', args) .then(tryMapWith(item => { return new types.CodeLens( @@ -475,7 +520,7 @@ export class ExtHostApiCommands { } private _executeTaskProvider(): Thenable { - return this._tasks.executeTaskProvider(); + return this._tasks.fetchTasks(); } } @@ -486,4 +531,4 @@ function tryMapWith(f: (x: T) => R) { } return undefined; }; -} \ No newline at end of file +} diff --git a/src/vs/workbench/api/node/extHostConfiguration.ts b/src/vs/workbench/api/node/extHostConfiguration.ts index 93b2d217d8e..aa831b81768 100644 --- a/src/vs/workbench/api/node/extHostConfiguration.ts +++ b/src/vs/workbench/api/node/extHostConfiguration.ts @@ -14,7 +14,7 @@ import { ConfigurationTarget as ExtHostConfigurationTarget } from './extHostType import { IConfigurationData, ConfigurationTarget, IConfigurationModel } from 'vs/platform/configuration/common/configuration'; import { Configuration, ConfigurationChangeEvent, ConfigurationModel } from 'vs/platform/configuration/common/configurationModels'; import { WorkspaceConfigurationChangeEvent } from 'vs/workbench/services/configuration/common/configurationModels'; -import { StrictResourceMap } from 'vs/base/common/map'; +import { ResourceMap } from 'vs/base/common/map'; import { ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; import { isObject } from 'vs/base/common/types'; @@ -107,12 +107,16 @@ export class ExtHostConfiguration implements ExtHostConfigurationShape { return isObject(target) ? new Proxy(target, { get: (target: any, property: string) => { + if (typeof property === 'string' && property.toLowerCase() === 'tojson') { + cloneTarget(); + return () => clonedTarget; + } if (clonedConfig) { clonedTarget = clonedTarget ? clonedTarget : lookUp(clonedConfig, accessor); return clonedTarget[property]; } const result = target[property]; - if (typeof property === 'string' && property.toLowerCase() !== 'tojson') { + if (typeof property === 'string') { return cloneOnWriteProxy(result, `${accessor}.${property}`); } return result; @@ -205,7 +209,7 @@ export class ExtHostConfiguration implements ExtHostConfigurationShape { private _toConfigurationChangeEvent(data: IWorkspaceConfigurationChangeEventData): vscode.ConfigurationChangeEvent { const changedConfiguration = new ConfigurationModel(data.changedConfiguration.contents, data.changedConfiguration.keys, data.changedConfiguration.overrides); - const changedConfigurationByResource: StrictResourceMap = new StrictResourceMap(); + const changedConfigurationByResource: ResourceMap = new ResourceMap(); for (const key of Object.keys(data.changedConfigurationByResource)) { const resource = URI.parse(key); const model = data.changedConfigurationByResource[key]; @@ -221,11 +225,11 @@ export class ExtHostConfiguration implements ExtHostConfigurationShape { const defaultConfiguration = ExtHostConfiguration.parseConfigurationModel(data.defaults); const userConfiguration = ExtHostConfiguration.parseConfigurationModel(data.user); const workspaceConfiguration = ExtHostConfiguration.parseConfigurationModel(data.workspace); - const folders: StrictResourceMap = Object.keys(data.folders).reduce((result, key) => { + const folders: ResourceMap = Object.keys(data.folders).reduce((result, key) => { result.set(URI.parse(key), ExtHostConfiguration.parseConfigurationModel(data.folders[key])); return result; - }, new StrictResourceMap()); - return new Configuration(defaultConfiguration, userConfiguration, workspaceConfiguration, folders, new ConfigurationModel(), new StrictResourceMap(), false); + }, new ResourceMap()); + return new Configuration(defaultConfiguration, userConfiguration, workspaceConfiguration, folders, new ConfigurationModel(), new ResourceMap(), false); } private static parseConfigurationModel(model: IConfigurationModel): ConfigurationModel { diff --git a/src/vs/workbench/api/node/extHostDebugService.ts b/src/vs/workbench/api/node/extHostDebugService.ts index 39e58cacd5f..b969522ba5e 100644 --- a/src/vs/workbench/api/node/extHostDebugService.ts +++ b/src/vs/workbench/api/node/extHostDebugService.ts @@ -4,6 +4,9 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; +import * as paths from 'vs/base/common/paths'; +import { Schemas } from 'vs/base/common/network'; +import URI, { UriComponents } from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import { Event, Emitter } from 'vs/base/common/event'; import { asWinJsPromise } from 'vs/base/common/async'; @@ -11,18 +14,24 @@ import { MainContext, MainThreadDebugServiceShape, ExtHostDebugServiceShape, DebugSessionUUID, IMainContext, IBreakpointsDeltaDto, ISourceMultiBreakpointDto, IFunctionBreakpointDto } from 'vs/workbench/api/node/extHost.protocol'; -import { ExtHostWorkspace } from 'vs/workbench/api/node/extHostWorkspace'; - import * as vscode from 'vscode'; -import URI, { UriComponents } from 'vs/base/common/uri'; import { Disposable, Position, Location, SourceBreakpoint, FunctionBreakpoint } from 'vs/workbench/api/node/extHostTypes'; import { generateUuid } from 'vs/base/common/uuid'; +import { DebugAdapter, convertToVSCPaths, convertToDAPaths } from 'vs/workbench/parts/debug/node/debugAdapter'; +import { ExtHostWorkspace } from 'vs/workbench/api/node/extHostWorkspace'; +import { ExtHostExtensionService } from 'vs/workbench/api/node/extHostExtensionService'; +import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/node/extHostDocumentsAndEditors'; +import { IAdapterExecutable, ITerminalSettings, IDebuggerContribution, IConfig } from 'vs/workbench/parts/debug/common/debug'; +import { getTerminalLauncher } from 'vs/workbench/parts/debug/node/terminals'; +import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; +import { VariableResolver } from 'vs/workbench/services/configurationResolver/node/variableResolver'; +import { IConfigurationResolverService } from '../../services/configurationResolver/common/configurationResolver'; +import { IStringDictionary } from 'vs/base/common/collections'; +import { ExtHostConfiguration } from './extHostConfiguration'; export class ExtHostDebugService implements ExtHostDebugServiceShape { - private _workspace: ExtHostWorkspace; - private _handleCounter: number; private _handlers: Map; @@ -52,10 +61,17 @@ export class ExtHostDebugService implements ExtHostDebugServiceShape { private readonly _onDidChangeBreakpoints: Emitter; + private _debugAdapters: Map; - constructor(mainContext: IMainContext, workspace: ExtHostWorkspace) { + private _variableResolver: IConfigurationResolverService; - this._workspace = workspace; + + constructor(mainContext: IMainContext, + private _workspace: ExtHostWorkspace, + private _extensionService: ExtHostExtensionService, + private _editorsService: ExtHostDocumentsAndEditors, + private _configurationService: ExtHostConfiguration + ) { this._handleCounter = 0; this._handlers = new Map(); @@ -77,6 +93,86 @@ export class ExtHostDebugService implements ExtHostDebugServiceShape { this._breakpoints = new Map(); this._breakpointEventsActive = false; + + this._debugAdapters = new Map(); + + // register all debug extensions + const debugTypes: string[] = []; + for (const ed of this._extensionService.getAllExtensionDescriptions()) { + if (ed.contributes) { + const debuggers = ed.contributes['debuggers']; + if (debuggers && debuggers.length > 0) { + for (const dbg of debuggers) { + // only debugger contributions with a "label" are considered a "main" debugger contribution + if (dbg.type && dbg.label) { + debugTypes.push(dbg.type); + } + } + } + } + } + if (debugTypes.length > 0) { + this._debugServiceProxy.$registerDebugTypes(debugTypes); + } + } + + public $runInTerminal(args: DebugProtocol.RunInTerminalRequestArguments, config: ITerminalSettings): TPromise { + const terminalLauncher = getTerminalLauncher(); + if (terminalLauncher) { + return terminalLauncher.runInTerminal(args, config); + } + return void 0; + } + + public $substituteVariables(folderUri: UriComponents | undefined, config: IConfig): TPromise { + if (!this._variableResolver) { + this._variableResolver = new ExtHostVariableResolverService(this._workspace, this._editorsService, this._configurationService); + } + const folder = this.getFolder(folderUri); + return asWinJsPromise(token => DebugAdapter.substituteVariables(folder, config, this._variableResolver)); + } + + public $startDASession(handle: number, debugType: string, adpaterExecutable: IAdapterExecutable | null): TPromise { + const mythis = this; + + const da = new class extends DebugAdapter { + + // DA -> VS Code + public acceptMessage(message: DebugProtocol.ProtocolMessage) { + convertToVSCPaths(message, source => { + if (paths.isAbsolute(source.path)) { + (source).path = URI.file(source.path); + } + }); + mythis._debugServiceProxy.$acceptDAMessage(handle, message); + } + + }(debugType, adpaterExecutable, this._extensionService.getAllExtensionDescriptions()); + + this._debugAdapters.set(handle, da); + da.onError(err => this._debugServiceProxy.$acceptDAError(handle, err.name, err.message, err.stack)); + da.onExit(code => this._debugServiceProxy.$acceptDAExit(handle, code, null)); + return da.startSession(); + } + + public $sendDAMessage(handle: number, message: DebugProtocol.ProtocolMessage): TPromise { + // VS Code -> DA + convertToDAPaths(message, source => { + if (typeof source.path === 'object') { + source.path = URI.revive(source.path).fsPath; + } + }); + const da = this._debugAdapters.get(handle); + if (da) { + da.sendMessage(message); + } + return void 0; + } + + public $stopDASession(handle: number): TPromise { + const da = this._debugAdapters.get(handle); + this._debugAdapters.delete(handle); + return da ? da.stopSession() : void 0; } private startBreakpoints() { @@ -429,3 +525,70 @@ export class ExtHostDebugConsole implements vscode.DebugConsole { this.append(value + '\n'); } } + +export class ExtHostVariableResolverService implements IConfigurationResolverService { + + _serviceBrand: any; + _variableResolver: VariableResolver; + + constructor(workspace: ExtHostWorkspace, editors: ExtHostDocumentsAndEditors, configuration: ExtHostConfiguration) { + this._variableResolver = new VariableResolver({ + getFolderUri: (folderName: string): URI => { + const folders = workspace.getWorkspaceFolders(); + const found = folders.filter(f => f.name === folderName); + if (found && found.length > 0) { + return found[0].uri; + } + return undefined; + }, + getWorkspaceFolderCount: (): number => { + return workspace.getWorkspaceFolders().length; + }, + getConfigurationValue: (folderUri: URI, section: string) => { + return configuration.getConfiguration(undefined, folderUri).get(section); + }, + getExecPath: (): string | undefined => { + return undefined; // does not exist in EH + }, + getFilePath: (): string | undefined => { + const activeEditor = editors.activeEditor(); + if (activeEditor) { + const resource = activeEditor.document.uri; + if (resource.scheme === Schemas.file) { + return paths.normalize(resource.fsPath, true); + } + } + return undefined; + }, + getSelectedText: (): string | undefined => { + const activeEditor = editors.activeEditor(); + if (activeEditor && !activeEditor.selection.isEmpty) { + return activeEditor.document.getText(activeEditor.selection); + } + return undefined; + }, + getLineNumber: (): string => { + const activeEditor = editors.activeEditor(); + if (activeEditor) { + return String(activeEditor.selection.end.line + 1); + } + return undefined; + } + }, process.env); + } + + public resolve(root: IWorkspaceFolder, value: string): string; + public resolve(root: IWorkspaceFolder, value: string[]): string[]; + public resolve(root: IWorkspaceFolder, value: IStringDictionary): IStringDictionary; + public resolve(root: IWorkspaceFolder, value: any): any { + return this._variableResolver.resolveAny(root ? root.uri : undefined, value); + } + + public resolveAny(root: IWorkspaceFolder, value: any): any { + return this._variableResolver.resolveAny(root ? root.uri : undefined, value); + } + + resolveInteractiveVariables(configuration: any, interactiveVariablesMap: { [key: string]: string; }): TPromise { + throw new Error('Method not implemented.'); + } +} \ No newline at end of file diff --git a/src/vs/workbench/api/node/extHostDecorations.ts b/src/vs/workbench/api/node/extHostDecorations.ts index 89987b137bb..77d9b441fc8 100644 --- a/src/vs/workbench/api/node/extHostDecorations.ts +++ b/src/vs/workbench/api/node/extHostDecorations.ts @@ -43,6 +43,10 @@ export class ExtHostDecorations implements ExtHostDecorationsShape { return TPromise.join(requests.map(request => { const { handle, uri, id } = request; const provider = this._provider.get(handle); + if (!provider) { + // might have been unregistered in the meantime + return void 0; + } return asWinJsPromise(token => provider.provideDecoration(URI.revive(uri), token)).then(data => { result[id] = data && [data.priority, data.bubble, data.title, data.abbreviation, data.color, data.source]; }, err => { diff --git a/src/vs/workbench/api/node/extHostDiagnostics.ts b/src/vs/workbench/api/node/extHostDiagnostics.ts index 342060762d7..60f96a01f2e 100644 --- a/src/vs/workbench/api/node/extHostDiagnostics.ts +++ b/src/vs/workbench/api/node/extHostDiagnostics.ts @@ -229,6 +229,7 @@ export class ExtHostDiagnostics implements ExtHostDiagnosticsShape { } } } + Object.freeze(uris); return { uris }; } diff --git a/src/vs/workbench/api/node/extHostDocumentData.ts b/src/vs/workbench/api/node/extHostDocumentData.ts index 2be3ac6908d..d42afcf7ba0 100644 --- a/src/vs/workbench/api/node/extHostDocumentData.ts +++ b/src/vs/workbench/api/node/extHostDocumentData.ts @@ -68,8 +68,7 @@ export class ExtHostDocumentData extends MirrorTextModel { this._document = { get uri() { return data._uri; }, get fileName() { return data._uri.fsPath; }, - // todo@remote - // documents from other fs-provider must not be untitled + // todo@remote -> https://github.com/Microsoft/vscode/issues/48269 get isUntitled() { return data._uri.scheme !== 'file'; }, get languageId() { return data._languageId; }, get version() { return data._versionId; }, diff --git a/src/vs/workbench/api/node/extHostDocuments.ts b/src/vs/workbench/api/node/extHostDocuments.ts index 05cae8cfcb6..54e17e42f71 100644 --- a/src/vs/workbench/api/node/extHostDocuments.ts +++ b/src/vs/workbench/api/node/extHostDocuments.ts @@ -137,6 +137,7 @@ export class ExtHostDocuments implements ExtHostDocumentsShape { contentChanges: events.changes.map((change) => { return { range: TypeConverters.toRange(change.range), + rangeOffset: change.rangeOffset, rangeLength: change.rangeLength, text: change.text }; diff --git a/src/vs/workbench/api/node/extHostExtensionActivator.ts b/src/vs/workbench/api/node/extHostExtensionActivator.ts index e1979889222..c37e7b2dfe7 100644 --- a/src/vs/workbench/api/node/extHostExtensionActivator.ts +++ b/src/vs/workbench/api/node/extHostExtensionActivator.ts @@ -127,6 +127,7 @@ export class ExtensionActivationTimesBuilder { export class ActivatedExtension { public readonly activationFailed: boolean; + public readonly activationFailedError: Error; public readonly activationTimes: ExtensionActivationTimes; public readonly module: IExtensionModule; public readonly exports: IExtensionAPI; @@ -134,12 +135,14 @@ export class ActivatedExtension { constructor( activationFailed: boolean, + activationFailedError: Error, activationTimes: ExtensionActivationTimes, module: IExtensionModule, exports: IExtensionAPI, subscriptions: IDisposable[] ) { this.activationFailed = activationFailed; + this.activationFailedError = activationFailedError; this.activationTimes = activationTimes; this.module = module; this.exports = exports; @@ -149,13 +152,13 @@ export class ActivatedExtension { export class EmptyExtension extends ActivatedExtension { constructor(activationTimes: ExtensionActivationTimes) { - super(false, activationTimes, { activate: undefined, deactivate: undefined }, undefined, []); + super(false, null, activationTimes, { activate: undefined, deactivate: undefined }, undefined, []); } } export class FailedExtension extends ActivatedExtension { - constructor(activationTimes: ExtensionActivationTimes) { - super(true, activationTimes, { activate: undefined, deactivate: undefined }, undefined, []); + constructor(activationError: Error) { + super(true, activationError, ExtensionActivationTimes.NONE, { activate: undefined, deactivate: undefined }, undefined, []); } } @@ -244,7 +247,8 @@ export class ExtensionsActivator { if (!depDesc) { // Error condition 1: unknown dependency this._host.showMessage(Severity.Error, nls.localize('unknownDep', "Extension '{1}' failed to activate. Reason: unknown dependency '{0}'.", depId, currentExtension.id)); - this._activatedExtensions[currentExtension.id] = new FailedExtension(ExtensionActivationTimes.NONE); + const error = new Error(`Unknown dependency '${depId}'`); + this._activatedExtensions[currentExtension.id] = new FailedExtension(error); return; } @@ -253,7 +257,9 @@ export class ExtensionsActivator { if (dep.activationFailed) { // Error condition 2: a dependency has already failed activation this._host.showMessage(Severity.Error, nls.localize('failedDep1', "Extension '{1}' failed to activate. Reason: dependency '{0}' failed to activate.", depId, currentExtension.id)); - this._activatedExtensions[currentExtension.id] = new FailedExtension(ExtensionActivationTimes.NONE); + const error = new Error(`Dependency ${depId} failed to activate`); + (error).detail = dep.activationFailedError; + this._activatedExtensions[currentExtension.id] = new FailedExtension(error); return; } } else { @@ -286,7 +292,8 @@ export class ExtensionsActivator { for (let i = 0, len = extensionDescriptions.length; i < len; i++) { // Error condition 3: dependency loop this._host.showMessage(Severity.Error, nls.localize('failedDep2', "Extension '{0}' failed to activate. Reason: more than 10 levels of dependencies (most likely a dependency loop).", extensionDescriptions[i].id)); - this._activatedExtensions[extensionDescriptions[i].id] = new FailedExtension(ExtensionActivationTimes.NONE); + const error = new Error('More than 10 levels of dependencies (most likely a dependency loop)'); + this._activatedExtensions[extensionDescriptions[i].id] = new FailedExtension(error); } return TPromise.as(void 0); } @@ -334,7 +341,7 @@ export class ExtensionsActivator { console.error('Activating extension `' + extensionDescription.id + '` failed: ', err.message); console.log('Here is the error stack: ', err.stack); // Treat the extension as being empty - return new FailedExtension(ExtensionActivationTimes.NONE); + return new FailedExtension(err); }).then((x: ActivatedExtension) => { this._activatedExtensions[extensionDescription.id] = x; delete this._activatingExtensions[extensionDescription.id]; diff --git a/src/vs/workbench/api/node/extHostExtensionService.ts b/src/vs/workbench/api/node/extHostExtensionService.ts index 8532e95c37e..8d3d62d08fb 100644 --- a/src/vs/workbench/api/node/extHostExtensionService.ts +++ b/src/vs/workbench/api/node/extHostExtensionService.ts @@ -208,6 +208,17 @@ export class ExtHostExtensionService implements ExtHostExtensionServiceShape { } } + public activateByIdWithErrors(extensionId: string, reason: ExtensionActivationReason): TPromise { + return this.activateById(extensionId, reason).then(() => { + const extension = this._activator.getActivatedExtension(extensionId); + if (extension.activationFailed) { + // activation failed => bubble up the error as the promise result + return TPromise.wrapError(extension.activationFailedError); + } + return void 0; + }); + } + public getAllExtensionDescriptions(): IExtensionDescription[] { return this._registry.getAllExtensionDescriptions(); } @@ -371,7 +382,7 @@ export class ExtHostExtensionService implements ExtHostExtensionServiceShape { }; return this._callActivateOptional(logService, extensionId, extensionModule, context, activationTimesBuilder).then((extensionExports) => { - return new ActivatedExtension(false, activationTimesBuilder.build(), extensionModule, extensionExports, context.subscriptions); + return new ActivatedExtension(false, null, activationTimesBuilder.build(), extensionModule, extensionExports, context.subscriptions); }); } @@ -423,7 +434,7 @@ function getTelemetryActivationEvent(extensionDescription: IExtensionDescription "TelemetryActivationEvent" : { "id": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" }, "name": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" }, - "publisherDisplayName": { "classification": "PublicPersonalData", "purpose": "FeatureInsight" }, + "publisherDisplayName": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "activationEvents": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "isBuiltin": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } } diff --git a/src/vs/workbench/api/node/extHostFileSystem.ts b/src/vs/workbench/api/node/extHostFileSystem.ts index 00dfa69b6df..0a1264901fa 100644 --- a/src/vs/workbench/api/node/extHostFileSystem.ts +++ b/src/vs/workbench/api/node/extHostFileSystem.ts @@ -6,15 +6,17 @@ import URI, { UriComponents } from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; -import { MainContext, IMainContext, ExtHostFileSystemShape, MainThreadFileSystemShape } from './extHost.protocol'; +import { Event, mapEvent } from 'vs/base/common/event'; +import { MainContext, IMainContext, ExtHostFileSystemShape, MainThreadFileSystemShape, IFileChangeDto } from './extHost.protocol'; import * as vscode from 'vscode'; -import { IStat } from 'vs/platform/files/common/files'; +import * as files from 'vs/platform/files/common/files'; +import * as path from 'path'; import { IDisposable } from 'vs/base/common/lifecycle'; import { asWinJsPromise } from 'vs/base/common/async'; -import { IPatternInfo } from 'vs/platform/search/common/search'; import { values } from 'vs/base/common/map'; -import { Range } from 'vs/workbench/api/node/extHostTypes'; +import { Range, FileType, FileChangeType, FileChangeType2 } from 'vs/workbench/api/node/extHostTypes'; import { ExtHostLanguageFeatures } from 'vs/workbench/api/node/extHostLanguageFeatures'; +import { Schemas } from 'vs/base/common/network'; class FsLinkProvider implements vscode.DocumentLinkProvider { @@ -55,111 +57,254 @@ class FsLinkProvider implements vscode.DocumentLinkProvider { } } +class FileSystemProviderShim implements vscode.FileSystemProvider2 { + + _version: 9 = 9; + + onDidChangeFile: vscode.Event; + + constructor(private readonly _delegate: vscode.DeprecatedFileSystemProvider) { + if (!this._delegate.onDidChange) { + this.onDidChangeFile = Event.None; + } else { + this.onDidChangeFile = mapEvent(this._delegate.onDidChange, old => old.map(FileSystemProviderShim._modernizeFileChange)); + } + } + + watch(uri: vscode.Uri, options: {}): vscode.Disposable { + // does nothing because in the old API there was no notion of + // watch and provider decide what file events to generate... + return { dispose() { } }; + } + + stat(resource: vscode.Uri): Thenable { + return this._delegate.stat(resource).then(stat => FileSystemProviderShim._modernizeFileStat(stat)); + } + rename(oldUri: vscode.Uri, newUri: vscode.Uri): Thenable { + return this._delegate.move(oldUri, newUri).then(stat => FileSystemProviderShim._modernizeFileStat(stat)); + } + readDirectory(resource: vscode.Uri): Thenable<[string, vscode.FileStat2][]> { + return this._delegate.readdir(resource).then(tuples => { + return tuples.map(tuple => <[string, vscode.FileStat2]>[path.posix.basename(tuple[0].path), FileSystemProviderShim._modernizeFileStat(tuple[1])]); + }); + } + + private static _modernizeFileStat(stat: vscode.DeprecatedFileStat): vscode.FileStat2 { + let { mtime, size, type } = stat; + let isFile = false; + let isDirectory = false; + let isSymbolicLink = false; + + // no support for bitmask, effectively no support for symlinks + switch (type) { + case FileType.Dir: + isDirectory = true; + break; + case FileType.File: + isFile = true; + break; + case FileType.Symlink: + isSymbolicLink = true; + break; + } + return { mtime, size, isFile, isDirectory, isSymbolicLink }; + } + + private static _modernizeFileChange(e: vscode.DeprecatedFileChange): vscode.FileChange2 { + let { resource, type } = e; + let newType: vscode.FileChangeType2; + switch (type) { + case FileChangeType.Updated: + newType = FileChangeType2.Changed; + break; + case FileChangeType.Added: + newType = FileChangeType2.Created; + break; + case FileChangeType.Deleted: + newType = FileChangeType2.Deleted; + break; + + } + return { uri: resource, type: newType }; + } + + // --- delete/create file or folder + + delete(resource: vscode.Uri): Thenable { + return this._delegate.stat(resource).then(stat => { + if (stat.type === FileType.Dir) { + return this._delegate.rmdir(resource); + } else { + return this._delegate.unlink(resource); + } + }); + } + createDirectory(resource: vscode.Uri): Thenable { + return this._delegate.mkdir(resource).then(stat => FileSystemProviderShim._modernizeFileStat(stat)); + } + + // --- read/write + + readFile(resource: vscode.Uri): Thenable { + let chunks: Buffer[] = []; + return this._delegate.read(resource, 0, -1, { + report(data) { + chunks.push(Buffer.from(data)); + } + }).then(() => { + return Buffer.concat(chunks); + }); + } + + writeFile(resource: vscode.Uri, content: Uint8Array, options: files.FileOptions): Thenable { + return this._delegate.write(resource, content); + } +} + export class ExtHostFileSystem implements ExtHostFileSystemShape { private readonly _proxy: MainThreadFileSystemShape; - private readonly _fsProvider = new Map(); - private readonly _searchProvider = new Map(); private readonly _linkProvider = new FsLinkProvider(); + private readonly _fsProvider = new Map(); + private readonly _usedSchemes = new Set(); + private readonly _watches = new Map(); private _handlePool: number = 0; constructor(mainContext: IMainContext, extHostLanguageFeatures: ExtHostLanguageFeatures) { this._proxy = mainContext.getProxy(MainContext.MainThreadFileSystem); + this._usedSchemes.add(Schemas.file); + this._usedSchemes.add(Schemas.untitled); + this._usedSchemes.add(Schemas.vscode); + this._usedSchemes.add(Schemas.inMemory); + this._usedSchemes.add(Schemas.internal); + this._usedSchemes.add(Schemas.http); + this._usedSchemes.add(Schemas.https); + this._usedSchemes.add(Schemas.mailto); + this._usedSchemes.add(Schemas.data); + extHostLanguageFeatures.registerDocumentLinkProvider('*', this._linkProvider); } - registerFileSystemProvider(scheme: string, provider: vscode.FileSystemProvider) { + registerDeprecatedFileSystemProvider(scheme: string, provider: vscode.DeprecatedFileSystemProvider) { + return this.registerFileSystemProvider2(scheme, new FileSystemProviderShim(provider), { isCaseSensitive: false }); + } + + registerFileSystemProvider(scheme: string, provider: vscode.DeprecatedFileSystemProvider, newProvider: vscode.FileSystemProvider2) { + if (newProvider && newProvider._version === 9) { + return this.registerFileSystemProvider2(scheme, newProvider, { isCaseSensitive: false }); + } else if (provider) { + return this.registerFileSystemProvider2(scheme, new FileSystemProviderShim(provider), { isCaseSensitive: false }); + } else { + throw new Error('FAILED to register file system provider, the new provider does not meet the version-constraint and there is no old provider'); + } + } + + registerFileSystemProvider2(scheme: string, provider: vscode.FileSystemProvider2, options: { isCaseSensitive?: boolean }) { + + if (this._usedSchemes.has(scheme)) { + throw new Error(`a provider for the scheme '${scheme}' is already registered`); + } + const handle = this._handlePool++; this._linkProvider.add(scheme); + this._usedSchemes.add(scheme); this._fsProvider.set(handle, provider); - this._proxy.$registerFileSystemProvider(handle, scheme); - let reg: IDisposable; - if (provider.onDidChange) { - reg = provider.onDidChange(event => this._proxy.$onFileSystemChange(handle, event)); + + let capabilites = files.FileSystemProviderCapabilities.FileReadWrite; + if (options.isCaseSensitive) { + capabilites += files.FileSystemProviderCapabilities.PathCaseSensitive; } + if (typeof provider.copy === 'function') { + capabilites += files.FileSystemProviderCapabilities.FileFolderCopy; + } + + this._proxy.$registerFileSystemProvider(handle, scheme, capabilites); + + const subscription = provider.onDidChangeFile(event => { + let mapped: IFileChangeDto[] = []; + for (const e of event) { + let { uri: resource, type } = e; + if (resource.scheme !== scheme) { + // dropping events for wrong scheme + continue; + } + let newType: files.FileChangeType; + switch (type) { + case FileChangeType2.Changed: + newType = files.FileChangeType.UPDATED; + break; + case FileChangeType2.Created: + newType = files.FileChangeType.ADDED; + break; + case FileChangeType2.Deleted: + newType = files.FileChangeType.DELETED; + break; + } + mapped.push({ resource, type: newType }); + } + this._proxy.$onFileSystemChange(handle, mapped); + }); + return { dispose: () => { - if (reg) { - reg.dispose(); - } + subscription.dispose(); this._linkProvider.delete(scheme); + this._usedSchemes.delete(scheme); this._fsProvider.delete(handle); this._proxy.$unregisterProvider(handle); } }; } - registerSearchProvider(scheme: string, provider: vscode.SearchProvider) { - const handle = this._handlePool++; - this._searchProvider.set(handle, provider); - this._proxy.$registerSearchProvider(handle, scheme); - return { - dispose: () => { - this._searchProvider.delete(handle); - this._proxy.$unregisterProvider(handle); - } - }; + $stat(handle: number, resource: UriComponents): TPromise { + return asWinJsPromise(token => this._fsProvider.get(handle).stat(URI.revive(resource), {}, token)); } - $utimes(handle: number, resource: UriComponents, mtime: number, atime: number): TPromise { - return asWinJsPromise(token => this._fsProvider.get(handle).utimes(URI.revive(resource), mtime, atime)); + $readdir(handle: number, resource: UriComponents): TPromise<[string, files.IStat][], any> { + return asWinJsPromise(token => this._fsProvider.get(handle).readDirectory(URI.revive(resource), {}, token)); } - $stat(handle: number, resource: UriComponents): TPromise { - return asWinJsPromise(token => this._fsProvider.get(handle).stat(URI.revive(resource))); + + $readFile(handle: number, resource: UriComponents, opts: files.FileOptions): TPromise { + return asWinJsPromise(token => { + return this._fsProvider.get(handle).readFile(URI.revive(resource), opts, token); + }).then(data => { + return Buffer.isBuffer(data) ? data.toString('base64') : Buffer.from(data.buffer, data.byteOffset, data.byteLength).toString('base64'); + }); } - $read(handle: number, session: number, offset: number, count: number, resource: UriComponents): TPromise { - const progress = { - report: chunk => { - this._proxy.$reportFileChunk(handle, session, [].slice.call(chunk)); - } - }; - return asWinJsPromise(token => this._fsProvider.get(handle).read(URI.revive(resource), offset, count, progress)); + + $writeFile(handle: number, resource: UriComponents, base64Content: string, opts: files.FileOptions): TPromise { + return asWinJsPromise(token => this._fsProvider.get(handle).writeFile(URI.revive(resource), Buffer.from(base64Content, 'base64'), opts, token)); } - $write(handle: number, resource: UriComponents, content: number[]): TPromise { - return asWinJsPromise(token => this._fsProvider.get(handle).write(URI.revive(resource), Buffer.from(content))); + + $delete(handle: number, resource: UriComponents): TPromise { + return asWinJsPromise(token => this._fsProvider.get(handle).delete(URI.revive(resource), {}, token)); } - $unlink(handle: number, resource: UriComponents): TPromise { - return asWinJsPromise(token => this._fsProvider.get(handle).unlink(URI.revive(resource))); + + $rename(handle: number, oldUri: UriComponents, newUri: UriComponents, opts: files.FileOptions): TPromise { + return asWinJsPromise(token => this._fsProvider.get(handle).rename(URI.revive(oldUri), URI.revive(newUri), opts, token)); } - $move(handle: number, resource: UriComponents, target: UriComponents): TPromise { - return asWinJsPromise(token => this._fsProvider.get(handle).move(URI.revive(resource), URI.revive(target))); + + $copy(handle: number, oldUri: UriComponents, newUri: UriComponents, opts: files.FileOptions): TPromise { + return asWinJsPromise(token => this._fsProvider.get(handle).copy(URI.revive(oldUri), URI.revive(newUri), opts, token)); } - $mkdir(handle: number, resource: UriComponents): TPromise { - return asWinJsPromise(token => this._fsProvider.get(handle).mkdir(URI.revive(resource))); + + $mkdir(handle: number, resource: UriComponents): TPromise { + return asWinJsPromise(token => this._fsProvider.get(handle).createDirectory(URI.revive(resource), {}, token)); } - $readdir(handle: number, resource: UriComponents): TPromise<[UriComponents, IStat][], any> { - return asWinJsPromise(token => this._fsProvider.get(handle).readdir(URI.revive(resource))); + + $watch(handle: number, session: number, resource: UriComponents, opts: files.IWatchOptions): void { + asWinJsPromise(token => { + let subscription = this._fsProvider.get(handle).watch(URI.revive(resource), opts); + this._watches.set(session, subscription); + }); } - $rmdir(handle: number, resource: UriComponents): TPromise { - return asWinJsPromise(token => this._fsProvider.get(handle).rmdir(URI.revive(resource))); - } - $provideFileSearchResults(handle: number, session: number, query: string): TPromise { - const provider = this._searchProvider.get(handle); - if (!provider.provideFileSearchResults) { - return TPromise.as(undefined); + + $unwatch(handle: number, session: number): void { + let subscription = this._watches.get(session); + if (subscription) { + subscription.dispose(); + this._watches.delete(session); } - const progress = { - report: (uri) => { - this._proxy.$handleFindMatch(handle, session, uri); - } - }; - return asWinJsPromise(token => provider.provideFileSearchResults(query, progress, token)); - } - $provideTextSearchResults(handle: number, session: number, pattern: IPatternInfo, options: { includes: string[], excludes: string[] }): TPromise { - const provider = this._searchProvider.get(handle); - if (!provider.provideTextSearchResults) { - return TPromise.as(undefined); - } - const progress = { - report: (data: vscode.TextSearchResult) => { - this._proxy.$handleFindMatch(handle, session, [data.uri, { - lineNumber: data.range.start.line, - preview: data.preview.leading + data.preview.matching + data.preview.trailing, - offsetAndLengths: [[data.preview.leading.length, data.preview.matching.length]] - }]); - } - }; - return asWinJsPromise(token => provider.provideTextSearchResults(pattern, options, progress, token)); } } diff --git a/src/vs/workbench/api/node/extHostLanguageFeatures.ts b/src/vs/workbench/api/node/extHostLanguageFeatures.ts index 25ae4ba089a..2d14359a8b1 100644 --- a/src/vs/workbench/api/node/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/node/extHostLanguageFeatures.ts @@ -9,7 +9,7 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { mixin } from 'vs/base/common/objects'; import * as vscode from 'vscode'; import * as TypeConverters from 'vs/workbench/api/node/extHostTypeConverters'; -import { Range, Disposable, CompletionList, SnippetString, Color, CodeActionKind } from 'vs/workbench/api/node/extHostTypes'; +import { Range, Disposable, CompletionList, SnippetString, CodeActionKind } from 'vs/workbench/api/node/extHostTypes'; import { ISingleEditOperation } from 'vs/editor/common/model'; import * as modes from 'vs/editor/common/modes'; import { ExtHostHeapService } from 'vs/workbench/api/node/extHostHeapService'; @@ -22,6 +22,7 @@ import { regExpLeadsToEndlessLoop } from 'vs/base/common/strings'; import { IPosition } from 'vs/editor/common/core/position'; import { IRange } from 'vs/editor/common/core/range'; import { isFalsyOrEmpty } from 'vs/base/common/arrays'; +import { isObject } from 'vs/base/common/types'; // --- adapter @@ -466,14 +467,14 @@ class NavigateTypeAdapter { class RenameAdapter { - static supportsResolving(provider: vscode.RenameProvider2): boolean { + static supportsResolving(provider: vscode.RenameProvider): boolean { return typeof provider.resolveRenameLocation === 'function'; } private _documents: ExtHostDocuments; - private _provider: vscode.RenameProvider2; + private _provider: vscode.RenameProvider; - constructor(documents: ExtHostDocuments, provider: vscode.RenameProvider2) { + constructor(documents: ExtHostDocuments, provider: vscode.RenameProvider) { this._documents = documents; this._provider = provider; } @@ -506,7 +507,7 @@ class RenameAdapter { }); } - resolveRenameLocation(resource: URI, position: IPosition): TPromise { + resolveRenameLocation(resource: URI, position: IPosition): TPromise { if (typeof this._provider.resolveRenameLocation !== 'function') { return TPromise.as(undefined); } @@ -514,23 +515,32 @@ class RenameAdapter { let doc = this._documents.getDocumentData(resource).document; let pos = TypeConverters.toPosition(position); - return asWinJsPromise(token => this._provider.resolveRenameLocation(doc, pos, token)).then(context => { - if (!context) { + return asWinJsPromise(token => this._provider.resolveRenameLocation(doc, pos, token)).then(rangeOrLocation => { + + let range: vscode.Range; + let text: string; + if (Range.isRange(rangeOrLocation)) { + range = rangeOrLocation; + text = doc.getText(rangeOrLocation); + + } else if (isObject(rangeOrLocation)) { + range = rangeOrLocation.range; + text = rangeOrLocation.placeholder; + } + + if (!range) { return undefined; } - if (context.range && (!context.range.isSingleLine || context.range.start.line !== pos.line)) { - console.warn('INVALID rename context, range must be single line and on the same line'); + + if (!range.contains(pos)) { + console.warn('INVALID rename location: range must contain position'); return undefined; } - return { - range: TypeConverters.fromRange(context.range), - text: context.newName || doc.getText(context.range) - }; + return { range: TypeConverters.fromRange(range), text }; }); } } - class SuggestAdapter { static supportsResolving(provider: vscode.CompletionItemProvider): boolean { @@ -784,7 +794,7 @@ class ColorProviderAdapter { const colorInfos: IRawColorInfo[] = colors.map(ci => { return { - color: [ci.color.red, ci.color.green, ci.color.blue, ci.color.alpha] as [number, number, number, number], + color: TypeConverters.Color.from(ci.color), range: TypeConverters.fromRange(ci.range) }; }); @@ -796,7 +806,7 @@ class ColorProviderAdapter { provideColorPresentations(resource: URI, raw: IRawColorInfo): TPromise { const document = this._documents.getDocumentData(resource).document; const range = TypeConverters.toRange(raw.range); - const color = new Color(raw.color[0], raw.color[1], raw.color[2], raw.color[3]); + const color = TypeConverters.Color.to(raw.color); return asWinJsPromise(token => this._provider.provideColorPresentations(color, { document, range }, token)).then(value => { return value.map(TypeConverters.ColorPresentation.from); }); @@ -807,16 +817,16 @@ class FoldingProviderAdapter { constructor( private _documents: ExtHostDocuments, - private _provider: vscode.FoldingProvider + private _provider: vscode.FoldingRangeProvider ) { } - provideFoldingRanges(resource: URI, context: modes.FoldingContext): TPromise { + provideFoldingRanges(resource: URI, context: modes.FoldingContext): TPromise { const doc = this._documents.getDocumentData(resource).document; - return asWinJsPromise(token => this._provider.provideFoldingRanges(doc, context, token)).then(list => { - if (!Array.isArray(list.ranges)) { + return asWinJsPromise(token => this._provider.provideFoldingRanges(doc, context, token)).then(ranges => { + if (!Array.isArray(ranges)) { return void 0; } - return TypeConverters.FoldingRangeList.from(list); + return ranges.map(TypeConverters.FoldingRange.from); }); } } @@ -827,10 +837,15 @@ type Adapter = OutlineAdapter | CodeLensAdapter | DefinitionAdapter | HoverAdapt | SuggestAdapter | SignatureHelpAdapter | LinkProviderAdapter | ImplementationAdapter | TypeDefinitionAdapter | ColorProviderAdapter | FoldingProviderAdapter; +export interface ISchemeTransformer { + transformOutgoing(scheme: string): string; +} + export class ExtHostLanguageFeatures implements ExtHostLanguageFeaturesShape { private static _handlePool: number = 0; + private readonly _schemeTransformer: ISchemeTransformer; private _proxy: MainThreadLanguageFeaturesShape; private _documents: ExtHostDocuments; private _commands: ExtHostCommands; @@ -840,11 +855,13 @@ export class ExtHostLanguageFeatures implements ExtHostLanguageFeaturesShape { constructor( mainContext: IMainContext, + schemeTransformer: ISchemeTransformer, documents: ExtHostDocuments, commands: ExtHostCommands, heapMonitor: ExtHostHeapService, diagnostics: ExtHostDiagnostics ) { + this._schemeTransformer = schemeTransformer; this._proxy = mainContext.getProxy(MainContext.MainThreadLanguageFeatures); this._documents = documents; this._commands = commands; @@ -881,6 +898,9 @@ export class ExtHostLanguageFeatures implements ExtHostLanguageFeaturesShape { } private _transformScheme(scheme: string): string { + if (this._schemeTransformer && typeof scheme === 'string') { + return this._schemeTransformer.transformOutgoing(scheme); + } return scheme; } @@ -1017,9 +1037,9 @@ export class ExtHostLanguageFeatures implements ExtHostLanguageFeaturesShape { // --- quick fix - registerCodeActionProvider(selector: vscode.DocumentSelector, provider: vscode.CodeActionProvider): vscode.Disposable { + registerCodeActionProvider(selector: vscode.DocumentSelector, provider: vscode.CodeActionProvider, metadata?: vscode.CodeActionProviderMetadata): vscode.Disposable { const handle = this._addNewAdapter(new CodeActionAdapter(this._documents, this._commands.converter, this._diagnostics, provider)); - this._proxy.$registerQuickFixSupport(handle, this._transformDocumentSelector(selector)); + this._proxy.$registerQuickFixSupport(handle, this._transformDocumentSelector(selector), metadata && metadata.providedCodeActionKinds ? metadata.providedCodeActionKinds.map(kind => kind.value) : undefined); return this._createDisposable(handle); } @@ -1092,7 +1112,7 @@ export class ExtHostLanguageFeatures implements ExtHostLanguageFeaturesShape { return this._withAdapter(handle, RenameAdapter, adapter => adapter.provideRenameEdits(URI.revive(resource), position, newName)); } - $resolveRenameLocation(handle: number, resource: URI, position: IPosition): TPromise { + $resolveRenameLocation(handle: number, resource: URI, position: IPosition): TPromise { return this._withAdapter(handle, RenameAdapter, adapter => adapter.resolveRenameLocation(resource, position)); } @@ -1158,13 +1178,13 @@ export class ExtHostLanguageFeatures implements ExtHostLanguageFeaturesShape { return this._withAdapter(handle, ColorProviderAdapter, adapter => adapter.provideColorPresentations(URI.revive(resource), colorInfo)); } - registerFoldingProvider(selector: vscode.DocumentSelector, provider: vscode.FoldingProvider): vscode.Disposable { + registerFoldingRangeProvider(selector: vscode.DocumentSelector, provider: vscode.FoldingRangeProvider): vscode.Disposable { const handle = this._addNewAdapter(new FoldingProviderAdapter(this._documents, provider)); - this._proxy.$registerFoldingProvider(handle, this._transformDocumentSelector(selector)); + this._proxy.$registerFoldingRangeProvider(handle, this._transformDocumentSelector(selector)); return this._createDisposable(handle); } - $provideFoldingRanges(handle: number, resource: UriComponents, context: vscode.FoldingContext): TPromise { + $provideFoldingRanges(handle: number, resource: UriComponents, context: vscode.FoldingContext): TPromise { return this._withAdapter(handle, FoldingProviderAdapter, adapter => adapter.provideFoldingRanges(URI.revive(resource), context)); } diff --git a/src/vs/workbench/api/node/extHostProgress.ts b/src/vs/workbench/api/node/extHostProgress.ts index 07309949af0..a9f8427ef0a 100644 --- a/src/vs/workbench/api/node/extHostProgress.ts +++ b/src/vs/workbench/api/node/extHostProgress.ts @@ -4,13 +4,14 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import { Progress, ProgressOptions } from 'vscode'; +import { ProgressOptions } from 'vscode'; import { MainThreadProgressShape, ExtHostProgressShape } from './extHost.protocol'; import { ProgressLocation } from './extHostTypeConverters'; import { IExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; -import { IProgressStep } from 'vs/platform/progress/common/progress'; +import { IProgressStep, Progress } from 'vs/platform/progress/common/progress'; import { localize } from 'vs/nls'; import { CancellationTokenSource, CancellationToken } from 'vs/base/common/cancellation'; +import { debounce } from 'vs/base/common/decorators'; export class ExtHostProgress implements ExtHostProgressShape { @@ -37,12 +38,6 @@ export class ExtHostProgress implements ExtHostProgressShape { this._mapHandleToCancellationSource.set(handle, source); } - const progress = { - report: (p: IProgressStep) => { - this._proxy.$progressReport(handle, p); - } - }; - const progressEnd = (handle: number): void => { this._proxy.$progressEnd(handle); this._mapHandleToCancellationSource.delete(handle); @@ -54,7 +49,7 @@ export class ExtHostProgress implements ExtHostProgressShape { let p: Thenable; try { - p = task(progress, cancellable ? source.token : CancellationToken.None); + p = task(new ProgressCallback(this._proxy, handle), cancellable ? source.token : CancellationToken.None); } catch (err) { progressEnd(handle); throw err; @@ -73,3 +68,23 @@ export class ExtHostProgress implements ExtHostProgressShape { } } +function mergeProgress(result: IProgressStep, currentValue: IProgressStep): IProgressStep { + result.message = currentValue.message; + if (typeof currentValue.increment === 'number' && typeof result.message === 'number') { + result.increment += currentValue.increment; + } else if (typeof currentValue.increment === 'number') { + result.increment = currentValue.increment; + } + return result; +} + +class ProgressCallback extends Progress { + constructor(private _proxy: MainThreadProgressShape, private _handle: number) { + super(p => this.throttledReport(p)); + } + + @debounce(100, (result: IProgressStep, currentValue: IProgressStep) => mergeProgress(result, currentValue), () => Object.create(null)) + throttledReport(p: IProgressStep): void { + this._proxy.$progressReport(this._handle, p); + } +} \ No newline at end of file diff --git a/src/vs/workbench/api/node/extHostQuickOpen.ts b/src/vs/workbench/api/node/extHostQuickOpen.ts index 2d32ed719fe..d2f5e8bbaec 100644 --- a/src/vs/workbench/api/node/extHostQuickOpen.ts +++ b/src/vs/workbench/api/node/extHostQuickOpen.ts @@ -29,9 +29,10 @@ export class ExtHostQuickOpen implements ExtHostQuickOpenShape { this._commands = commands; } + showQuickPick(itemsOrItemsPromise: QuickPickItem[] | Thenable, options: QuickPickOptions & { canSelectMany: true; }, token?: CancellationToken): Thenable; showQuickPick(itemsOrItemsPromise: string[] | Thenable, options?: QuickPickOptions, token?: CancellationToken): Thenable; showQuickPick(itemsOrItemsPromise: QuickPickItem[] | Thenable, options?: QuickPickOptions, token?: CancellationToken): Thenable; - showQuickPick(itemsOrItemsPromise: Item[] | Thenable, options?: QuickPickOptions, token: CancellationToken = CancellationToken.None): Thenable { + showQuickPick(itemsOrItemsPromise: Item[] | Thenable, options?: QuickPickOptions, token: CancellationToken = CancellationToken.None): Thenable { // clear state from last invocation this._onDidSelectItem = undefined; @@ -43,7 +44,8 @@ export class ExtHostQuickOpen implements ExtHostQuickOpenShape { placeHolder: options && options.placeHolder, matchOnDescription: options && options.matchOnDescription, matchOnDetail: options && options.matchOnDetail, - ignoreFocusLost: options && options.ignoreFocusOut + ignoreFocusLost: options && options.ignoreFocusOut, + canSelectMany: options && options.canPickMany }); const promise = TPromise.any([]>[quickPickWidget, itemsPromise]).then(values => { @@ -60,6 +62,7 @@ export class ExtHostQuickOpen implements ExtHostQuickOpenShape { let label: string; let description: string; let detail: string; + let picked: boolean; if (typeof item === 'string') { label = item; @@ -67,12 +70,14 @@ export class ExtHostQuickOpen implements ExtHostQuickOpenShape { label = item.label; description = item.description; detail = item.detail; + picked = item.picked; } pickItems.push({ label, description, handle, - detail + detail, + picked }); } @@ -89,6 +94,8 @@ export class ExtHostQuickOpen implements ExtHostQuickOpenShape { return quickPickWidget.then(handle => { if (typeof handle === 'number') { return items[handle]; + } else if (Array.isArray(handle)) { + return handle.map(h => items[h]); } return undefined; }); @@ -98,7 +105,7 @@ export class ExtHostQuickOpen implements ExtHostQuickOpenShape { return TPromise.wrapError(err); }); }); - return wireCancellationToken(token, promise, true); + return wireCancellationToken(token, promise, true); } $onItemSelected(handle: number): void { diff --git a/src/vs/workbench/api/node/extHostSearch.ts b/src/vs/workbench/api/node/extHostSearch.ts new file mode 100644 index 00000000000..714e9bfe766 --- /dev/null +++ b/src/vs/workbench/api/node/extHostSearch.ts @@ -0,0 +1,64 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { asWinJsPromise } from 'vs/base/common/async'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { IPatternInfo } from 'vs/platform/search/common/search'; +import * as vscode from 'vscode'; +import { ExtHostSearchShape, IMainContext, MainContext, MainThreadSearchShape } from './extHost.protocol'; + +export class ExtHostSearch implements ExtHostSearchShape { + + private readonly _proxy: MainThreadSearchShape; + private readonly _searchProvider = new Map(); + private _handlePool: number = 0; + + constructor(mainContext: IMainContext) { + this._proxy = mainContext.getProxy(MainContext.MainThreadSearch); + } + + registerSearchProvider(scheme: string, provider: vscode.SearchProvider) { + const handle = this._handlePool++; + this._searchProvider.set(handle, provider); + this._proxy.$registerSearchProvider(handle, scheme); + return { + dispose: () => { + this._searchProvider.delete(handle); + this._proxy.$unregisterProvider(handle); + } + }; + } + + $provideFileSearchResults(handle: number, session: number, query: string): TPromise { + const provider = this._searchProvider.get(handle); + if (!provider.provideFileSearchResults) { + return TPromise.as(undefined); + } + const progress = { + report: (uri) => { + this._proxy.$handleFindMatch(handle, session, uri); + } + }; + return asWinJsPromise(token => provider.provideFileSearchResults(query, progress, token)); + } + + $provideTextSearchResults(handle: number, session: number, pattern: IPatternInfo, options: { includes: string[], excludes: string[] }): TPromise { + const provider = this._searchProvider.get(handle); + if (!provider.provideTextSearchResults) { + return TPromise.as(undefined); + } + const progress = { + report: (data: vscode.TextSearchResult) => { + this._proxy.$handleFindMatch(handle, session, [data.uri, { + lineNumber: data.range.start.line, + preview: data.preview.leading + data.preview.matching + data.preview.trailing, + offsetAndLengths: [[data.preview.leading.length, data.preview.matching.length]] + }]); + } + }; + return asWinJsPromise(token => provider.provideTextSearchResults(pattern, options, progress, token)); + } +} diff --git a/src/vs/workbench/api/node/extHostTask.ts b/src/vs/workbench/api/node/extHostTask.ts index 828b31b8c96..024ad5f56b0 100644 --- a/src/vs/workbench/api/node/extHostTask.ts +++ b/src/vs/workbench/api/node/extHostTask.ts @@ -21,7 +21,7 @@ import { ExtHostWorkspace } from 'vs/workbench/api/node/extHostWorkspace'; import * as vscode from 'vscode'; import { TaskDefinitionDTO, TaskExecutionDTO, TaskPresentationOptionsDTO, ProcessExecutionOptionsDTO, ProcessExecutionDTO, - ShellExecutionOptionsDTO, ShellExecutionDTO, TaskDTO, TaskHandleDTO + ShellExecutionOptionsDTO, ShellExecutionDTO, TaskDTO, TaskHandleDTO, TaskFilterDTO } from '../shared/tasks'; export { TaskExecutionDTO }; @@ -613,6 +613,7 @@ namespace TaskDTO { if (!execution || !definition || !scope) { return undefined; } + let group = (value.group as types.TaskGroup) ? (value.group as types.TaskGroup).id : undefined; let result: TaskDTO = { _id: (value as types.Task)._id, definition, @@ -624,7 +625,7 @@ namespace TaskDTO { }, execution, isBackground: value.isBackground, - group: (value.group as types.TaskGroup).id, + group: group, presentationOptions: TaskPresentationOptionsDTO.from(value.presentationOptions), problemMatchers: value.problemMatchers, hasDefinedMatchers: (value as types.Task).hasDefinedMatchers @@ -674,18 +675,40 @@ namespace TaskDTO { } } +namespace TaskFilterDTO { + export function from(value: vscode.TaskFilter): TaskFilterDTO { + return value; + } + + export function to(value: TaskFilterDTO): vscode.TaskFilter { + if (!value) { + return undefined; + } + return Objects.assign(Object.create(null), value); + } +} + class TaskExecutionImpl implements vscode.TaskExecution { - constructor(readonly _id: string) { + constructor(readonly _id: string, private readonly _task: vscode.Task, private readonly _tasks: ExtHostTask) { + } + + get task(): vscode.Task { + return this._task; + } + + public terminate(): void { + this._tasks.terminateTask(this); } } namespace TaskExecutionDTO { - export function to(value: TaskExecutionDTO): vscode.TaskExecution { - return new TaskExecutionImpl(value.id); + export function to(value: TaskExecutionDTO, tasks: ExtHostTask): vscode.TaskExecution { + return new TaskExecutionImpl(value.id, TaskDTO.to(value.task, tasks.extHostWorkspace), tasks); } export function from(value: vscode.TaskExecution): TaskExecutionDTO { return { - id: (value as TaskExecutionImpl)._id + id: (value as TaskExecutionImpl)._id, + task: undefined }; } } @@ -701,6 +724,7 @@ export class ExtHostTask implements ExtHostTaskShape { private _extHostWorkspace: ExtHostWorkspace; private _handleCounter: number; private _handlers: Map; + private _taskExecutions: Map; private readonly _onDidExecuteTask: Emitter = new Emitter(); private readonly _onDidTerminateTask: Emitter = new Emitter(); @@ -710,6 +734,11 @@ export class ExtHostTask implements ExtHostTaskShape { this._extHostWorkspace = extHostWorkspace; this._handleCounter = 0; this._handlers = new Map(); + this._taskExecutions = new Map(); + } + + public get extHostWorkspace(): ExtHostWorkspace { + return this._extHostWorkspace; } public registerTaskProvider(extension: IExtensionDescription, provider: vscode.TaskProvider): vscode.Disposable { @@ -725,8 +754,8 @@ export class ExtHostTask implements ExtHostTaskShape { }); } - public executeTaskProvider(): Thenable { - return this._proxy.$executeTaskProvider().then((values) => { + public fetchTasks(filter?: vscode.TaskFilter): Thenable { + return this._proxy.$fetchTasks(TaskFilterDTO.from(filter)).then((values) => { let result: vscode.Task[] = []; for (let value of values) { let task = TaskDTO.to(value, this._extHostWorkspace); @@ -742,18 +771,28 @@ export class ExtHostTask implements ExtHostTaskShape { let tTask = (task as types.Task); // We have a preserved ID. So the task didn't change. if (tTask._id !== void 0) { - return this._proxy.$executeTask(TaskHandleDTO.from(tTask)).then(value => TaskExecutionDTO.to(value)); + return this._proxy.$executeTask(TaskHandleDTO.from(tTask)).then(value => this.getTaskExecution(value, task)); } else { - return this._proxy.$executeTask(TaskDTO.from(task, extension)).then(value => TaskExecutionDTO.to(value)); + let dto = TaskDTO.from(task, extension); + if (dto === void 0) { + return Promise.reject(new Error('Task is not valid')); + } + return this._proxy.$executeTask(dto).then(value => this.getTaskExecution(value, task)); } } public $taskStarted(execution: TaskExecutionDTO): void { this._onDidExecuteTask.fire({ - execution: TaskExecutionDTO.to(execution) + execution: this.getTaskExecution(execution) }); } + get taskExecutions(): vscode.TaskExecution[] { + let result: vscode.TaskExecution[] = []; + this._taskExecutions.forEach(value => result.push(value)); + return result; + } + get onDidStartTask(): Event { return this._onDidExecuteTask.event; } @@ -762,12 +801,14 @@ export class ExtHostTask implements ExtHostTaskShape { if (!(execution instanceof TaskExecutionImpl)) { throw new Error('No valid task execution provided'); } - return this._proxy.$terminateTask(TaskExecutionDTO.from(execution)); + return this._proxy.$terminateTask((execution as TaskExecutionImpl)._id); } public $taskEnded(execution: TaskExecutionDTO): void { + const _execution = this.getTaskExecution(execution); + this._taskExecutions.delete(execution.id); this._onDidTerminateTask.fire({ - execution: TaskExecutionDTO.to(execution) + execution: _execution }); } @@ -792,4 +833,14 @@ export class ExtHostTask implements ExtHostTaskShape { private nextHandle(): number { return this._handleCounter++; } + + private getTaskExecution(execution: TaskExecutionDTO, task?: vscode.Task): TaskExecutionImpl { + let result: TaskExecutionImpl = this._taskExecutions.get(execution.id); + if (result) { + return result; + } + result = new TaskExecutionImpl(execution.id, task ? task : TaskDTO.to(execution.task, this._extHostWorkspace), this); + this._taskExecutions.set(execution.id, result); + return result; + } } diff --git a/src/vs/workbench/api/node/extHostTerminalService.ts b/src/vs/workbench/api/node/extHostTerminalService.ts index 2a0f3081e2b..cb546c2e8d8 100644 --- a/src/vs/workbench/api/node/extHostTerminalService.ts +++ b/src/vs/workbench/api/node/extHostTerminalService.ts @@ -5,11 +5,18 @@ 'use strict'; import * as vscode from 'vscode'; +import * as cp from 'child_process'; +import * as os from 'os'; +import * as platform from 'vs/base/common/platform'; +import * as terminalEnvironment from 'vs/workbench/parts/terminal/node/terminalEnvironment'; +import Uri from 'vs/base/common/uri'; import { Event, Emitter } from 'vs/base/common/event'; -import { ExtHostTerminalServiceShape, MainContext, MainThreadTerminalServiceShape, IMainContext } from './extHost.protocol'; +import { ExtHostTerminalServiceShape, MainContext, MainThreadTerminalServiceShape, IMainContext, ShellLaunchConfigDto } from 'vs/workbench/api/node/extHost.protocol'; +import { IMessageFromTerminalProcess } from 'vs/workbench/parts/terminal/node/terminal'; +import { ExtHostConfiguration } from 'vs/workbench/api/node/extHostConfiguration'; +import { ILogService } from 'vs/platform/log/common/log'; export class ExtHostTerminal implements vscode.Terminal { - private _name: string; private _id: number; private _proxy: MainThreadTerminalServiceShape; @@ -20,21 +27,28 @@ export class ExtHostTerminal implements vscode.Terminal { constructor( proxy: MainThreadTerminalServiceShape, - name?: string, + name: string = '', + id?: number + ) { + this._proxy = proxy; + this._name = name; + if (id) { + this._id = id; + } + this._queuedRequests = []; + this._pidPromise = new Promise(c => { + this._pidPromiseComplete = c; + }); + } + + public create( shellPath?: string, shellArgs?: string[], cwd?: string, env?: { [key: string]: string }, waitOnExit?: boolean - ) { - this._name = name; - this._queuedRequests = []; - this._proxy = proxy; - this._pidPromise = new Promise(c => { - this._pidPromiseComplete = c; - }); - - this._proxy.$createTerminal(name, shellPath, shellArgs, cwd, env, waitOnExit).then((id) => { + ): void { + this._proxy.$createTerminal(this._name, shellPath, shellArgs, cwd, env, waitOnExit).then((id) => { this._id = id; this._queuedRequests.forEach((r) => { r.run(this._proxy, this._id); @@ -44,12 +58,10 @@ export class ExtHostTerminal implements vscode.Terminal { } public get name(): string { - this._checkDisposed(); return this._name; } public get processId(): Thenable { - this._checkDisposed(); return this._pidPromise; } @@ -76,8 +88,11 @@ export class ExtHostTerminal implements vscode.Terminal { } public _setProcessId(processId: number): void { - this._pidPromiseComplete(processId); - this._pidPromiseComplete = null; + // The event may fire 2 times when the panel is restored + if (this._pidPromiseComplete) { + this._pidPromiseComplete(processId); + this._pidPromiseComplete = null; + } } private _queueApiRequest(callback: (...args: any[]) => void, args: any[]) { @@ -97,25 +112,34 @@ export class ExtHostTerminal implements vscode.Terminal { } export class ExtHostTerminalService implements ExtHostTerminalServiceShape { - private readonly _onDidCloseTerminal: Emitter; + private readonly _onDidOpenTerminal: Emitter; private _proxy: MainThreadTerminalServiceShape; - private _terminals: ExtHostTerminal[]; + private _terminals: ExtHostTerminal[] = []; + private _terminalProcesses: { [id: number]: cp.ChildProcess } = {}; - constructor(mainContext: IMainContext) { + public get terminals(): ExtHostTerminal[] { return this._terminals; } + + constructor( + mainContext: IMainContext, + private _extHostConfiguration: ExtHostConfiguration, + private _logService: ILogService + ) { this._onDidCloseTerminal = new Emitter(); + this._onDidOpenTerminal = new Emitter(); this._proxy = mainContext.getProxy(MainContext.MainThreadTerminalService); - this._terminals = []; } public createTerminal(name?: string, shellPath?: string, shellArgs?: string[]): vscode.Terminal { - let terminal = new ExtHostTerminal(this._proxy, name, shellPath, shellArgs); + let terminal = new ExtHostTerminal(this._proxy, name); + terminal.create(shellPath, shellArgs); this._terminals.push(terminal); return terminal; } public createTerminalFromOptions(options: vscode.TerminalOptions): vscode.Terminal { - let terminal = new ExtHostTerminal(this._proxy, options.name, options.shellPath, options.shellArgs, options.cwd, options.env /*, options.waitOnExit*/); + let terminal = new ExtHostTerminal(this._proxy, options.name); + terminal.create(options.shellPath, options.shellArgs, options.cwd, options.env /*, options.waitOnExit*/); this._terminals.push(terminal); return terminal; } @@ -124,6 +148,10 @@ export class ExtHostTerminalService implements ExtHostTerminalServiceShape { return this._onDidCloseTerminal && this._onDidCloseTerminal.event; } + public get onDidOpenTerminal(): Event { + return this._onDidOpenTerminal && this._onDidOpenTerminal.event; + } + public $acceptTerminalClosed(id: number): void { let index = this._getTerminalIndexById(id); if (index === null) { @@ -134,6 +162,18 @@ export class ExtHostTerminalService implements ExtHostTerminalServiceShape { this._onDidCloseTerminal.fire(terminal); } + public $acceptTerminalOpened(id: number, name: string): void { + let index = this._getTerminalIndexById(id); + if (index !== null) { + // The terminal has already been created (via createTerminal*), only fire the event + this._onDidOpenTerminal.fire(this.terminals[index]); + return; + } + let terminal = new ExtHostTerminal(this._proxy, name, id); + this._terminals.push(terminal); + this._onDidOpenTerminal.fire(terminal); + } + public $acceptTerminalProcessId(id: number, processId: number): void { let terminal = this._getTerminalById(id); if (terminal) { @@ -141,6 +181,99 @@ export class ExtHostTerminalService implements ExtHostTerminalServiceShape { } } + public $createProcess(id: number, shellLaunchConfig: ShellLaunchConfigDto, cols: number, rows: number): void { + // TODO: This function duplicates a lot of TerminalProcessManager.createProcess, ideally + // they would be merged into a single implementation. + + const terminalConfig = this._extHostConfiguration.getConfiguration('terminal.integrated'); + + const locale = terminalConfig.get('setLocaleVariables') ? platform.locale : undefined; + if (!shellLaunchConfig.executable) { + // TODO: This duplicates some of TerminalConfigHelper.mergeDefaultShellPathAndArgs and should be merged + // this._configHelper.mergeDefaultShellPathAndArgs(shellLaunchConfig); + + const platformKey = platform.isWindows ? 'windows' : platform.isMacintosh ? 'osx' : 'linux'; + const shellConfigValue: string = terminalConfig.get(`shell.${platformKey}`); + const shellArgsConfigValue: string = terminalConfig.get(`shellArgs.${platformKey}`); + + shellLaunchConfig.executable = shellConfigValue; + shellLaunchConfig.args = shellArgsConfigValue; + } + + // TODO: Base the cwd on the last active workspace root + // const lastActiveWorkspaceRootUri = this._historyService.getLastActiveWorkspaceRoot('file'); + // this.initialCwd = terminalEnvironment.getCwd(shellLaunchConfig, lastActiveWorkspaceRootUri, this._configHelper); + const initialCwd = os.homedir(); + + // TODO: Pull in and resolve config settings + // // Resolve env vars from config and shell + // const lastActiveWorkspaceRoot = this._workspaceContextService.getWorkspaceFolder(lastActiveWorkspaceRootUri); + // const platformKey = platform.isWindows ? 'windows' : (platform.isMacintosh ? 'osx' : 'linux'); + // const envFromConfig = terminalEnvironment.resolveConfigurationVariables(this._configurationResolverService, { ...this._configHelper.config.env[platformKey] }, lastActiveWorkspaceRoot); + // const envFromShell = terminalEnvironment.resolveConfigurationVariables(this._configurationResolverService, { ...shellLaunchConfig.env }, lastActiveWorkspaceRoot); + // shellLaunchConfig.env = envFromShell; + + // Merge process env with the env from config + const parentEnv = { ...process.env }; + // terminalEnvironment.mergeEnvironments(parentEnv, envFromConfig); + + // Continue env initialization, merging in the env from the launch + // config and adding keys that are needed to create the process + const env = terminalEnvironment.createTerminalEnv(parentEnv, shellLaunchConfig, initialCwd, locale, cols, rows); + let cwd = Uri.parse(require.toUrl('../../parts/terminal/node')).fsPath; + const options = { env, cwd, execArgv: [] }; + + // Fork the process and listen for messages + this._logService.debug(`Terminal process launching on ext host`, options); + this._terminalProcesses[id] = cp.fork(Uri.parse(require.toUrl('bootstrap')).fsPath, ['--type=terminal'], options); + this._terminalProcesses[id].on('message', (message: IMessageFromTerminalProcess) => { + switch (message.type) { + case 'pid': this._proxy.$sendProcessPid(id, message.content); break; + case 'title': this._proxy.$sendProcessTitle(id, message.content); break; + case 'data': this._proxy.$sendProcessData(id, message.content); break; + } + }); + this._terminalProcesses[id].on('exit', (exitCode) => this._onProcessExit(id, exitCode)); + } + + public $acceptProcessInput(id: number, data: string): void { + if (this._terminalProcesses[id].connected) { + this._terminalProcesses[id].send({ event: 'input', data }); + } + } + + public $acceptProcessResize(id: number, cols: number, rows: number): void { + if (this._terminalProcesses[id].connected) { + try { + this._terminalProcesses[id].send({ event: 'resize', cols, rows }); + } catch (error) { + // We tried to write to a closed pipe / channel. + if (error.code !== 'EPIPE' && error.code !== 'ERR_IPC_CHANNEL_CLOSED') { + throw (error); + } + } + } + } + + public $acceptProcessShutdown(id: number): void { + if (this._terminalProcesses[id].connected) { + this._terminalProcesses[id].send({ event: 'shutdown' }); + } + } + + private _onProcessExit(id: number, exitCode: number): void { + // Remove listeners + const process = this._terminalProcesses[id]; + process.removeAllListeners('message'); + process.removeAllListeners('exit'); + + // Remove process reference + delete this._terminalProcesses[id]; + + // Send exit event to main side + this._proxy.$sendProcessExit(id, exitCode); + } + private _getTerminalById(id: number): ExtHostTerminal { let index = this._getTerminalIndexById(id); return index !== null ? this._terminals[index] : null; @@ -149,6 +282,7 @@ export class ExtHostTerminalService implements ExtHostTerminalServiceShape { private _getTerminalIndexById(id: number): number { let index: number = null; this._terminals.some((terminal, i) => { + // TODO: This shouldn't be cas let thisId = (terminal)._id; if (thisId === id) { index = i; @@ -161,7 +295,6 @@ export class ExtHostTerminalService implements ExtHostTerminalServiceShape { } class ApiRequest { - private _callback: (...args: any[]) => void; private _args: any[]; diff --git a/src/vs/workbench/api/node/extHostTreeViews.ts b/src/vs/workbench/api/node/extHostTreeViews.ts index b6ddcfa97a0..9cadef91101 100644 --- a/src/vs/workbench/api/node/extHostTreeViews.ts +++ b/src/vs/workbench/api/node/extHostTreeViews.ts @@ -152,7 +152,7 @@ class ExtHostTreeView extends Disposable { private resolveTreeNode(element: T, parent?: TreeNode): TPromise { return asWinJsPromise(() => this.dataProvider.getTreeItem(element)) - .then(extTreeItem => this.createHandle(element, extTreeItem, parent)) + .then(extTreeItem => this.createHandle(element, extTreeItem, parent, true)) .then(handle => this.getChildren(parent ? parent.item.handle : null) .then(() => { const cachedElement = this.getExtensionElement(handle); @@ -303,7 +303,7 @@ class ExtHostTreeView extends Disposable { return item; } - private createHandle(element: T, { id, label, resourceUri }: vscode.TreeItem, parent?: TreeNode): TreeItemHandle { + private createHandle(element: T, { id, label, resourceUri }: vscode.TreeItem, parent: TreeNode, first?: boolean): TreeItemHandle { if (id) { return `${ExtHostTreeView.ID_HANDLE_PREFIX}/${id}`; } @@ -316,7 +316,7 @@ class ExtHostTreeView extends Disposable { for (let counter = 0; counter <= childrenNodes.length; counter++) { const handle = `${prefix}/${counter}:${elementId}`; - if (!this.elements.has(handle) || existingHandle === handle) { + if (first || !this.elements.has(handle) || existingHandle === handle) { return handle; } } diff --git a/src/vs/workbench/api/node/extHostTypeConverters.ts b/src/vs/workbench/api/node/extHostTypeConverters.ts index bf5bf34612d..5c98f71fd82 100644 --- a/src/vs/workbench/api/node/extHostTypeConverters.ts +++ b/src/vs/workbench/api/node/extHostTypeConverters.ts @@ -83,7 +83,6 @@ export function fromPosition(position: types.Position): IPosition { return { lineNumber: position.line + 1, column: position.character + 1 }; } - export function fromDiagnostic(value: vscode.Diagnostic): IMarkerData { return { ...fromRange(value.range), @@ -245,7 +244,7 @@ export const TextEdit = { range: fromRange(edit.range) }; }, - to(edit: modes.TextEdit): vscode.TextEdit { + to(edit: modes.TextEdit): types.TextEdit { let result = new types.TextEdit(toRange(edit.range), edit.text); result.newEol = EndOfLine.to(edit.eol); return result; @@ -547,12 +546,15 @@ export namespace DocumentLink { } export namespace ColorPresentation { - export function to(colorPresentation: modes.IColorPresentation): vscode.ColorPresentation { - return { - label: colorPresentation.label, - textEdit: colorPresentation.textEdit ? TextEdit.to(colorPresentation.textEdit) : undefined, - additionalTextEdits: colorPresentation.additionalTextEdits ? colorPresentation.additionalTextEdits.map(value => TextEdit.to(value)) : undefined - }; + export function to(colorPresentation: modes.IColorPresentation): types.ColorPresentation { + let cp = new types.ColorPresentation(colorPresentation.label); + if (colorPresentation.textEdit) { + cp.textEdit = TextEdit.to(colorPresentation.textEdit); + } + if (colorPresentation.additionalTextEdits) { + cp.additionalTextEdits = colorPresentation.additionalTextEdits.map(value => TextEdit.to(value)); + } + return cp; } export function from(colorPresentation: vscode.ColorPresentation): modes.IColorPresentation { @@ -564,6 +566,15 @@ export namespace ColorPresentation { } } +export namespace Color { + export function to(c: [number, number, number, number]): types.Color { + return new types.Color(c[0], c[1], c[2], c[3]); + } + export function from(color: types.Color): [number, number, number, number] { + return [color.red, color.green, color.blue, color.alpha]; + } +} + export namespace TextDocumentSaveReason { export function to(reason: SaveReason): vscode.TextDocumentSaveReason { @@ -612,11 +623,9 @@ export namespace ProgressLocation { } } -export namespace FoldingRangeList { - export function from(rangeList: vscode.FoldingRangeList): modes.IFoldingRangeList { - return { - ranges: rangeList.ranges.map(r => ({ startLineNumber: r.startLine + 1, endLineNumber: r.endLine + 1, type: r.type })) - }; +export namespace FoldingRange { + export function from(r: vscode.FoldingRange): modes.FoldingRange { + return { start: r.start + 1, end: r.end + 1, kind: r.kind }; } } diff --git a/src/vs/workbench/api/node/extHostTypes.ts b/src/vs/workbench/api/node/extHostTypes.ts index 07ced944343..f75c83dbe18 100644 --- a/src/vs/workbench/api/node/extHostTypes.ts +++ b/src/vs/workbench/api/node/extHostTypes.ts @@ -208,7 +208,7 @@ export class Position { export class Range { - static isRange(thing: any): thing is Range { + static isRange(thing: any): thing is vscode.Range { if (thing instanceof Range) { return true; } @@ -903,6 +903,8 @@ export class CodeActionKind { public static readonly RefactorExtract = CodeActionKind.Refactor.append('extract'); public static readonly RefactorInline = CodeActionKind.Refactor.append('inline'); public static readonly RefactorRewrite = CodeActionKind.Refactor.append('rewrite'); + public static readonly Source = CodeActionKind.Empty.append('source'); + public static readonly SourceOrganizeImports = CodeActionKind.Source.append('organizeImports'); constructor( public readonly value: string @@ -1343,6 +1345,20 @@ export class ProcessExecution implements vscode.ProcessExecution { set options(value: vscode.ProcessExecutionOptions) { this._options = value; } + + public computeId(): string { + const hash = crypto.createHash('md5'); + hash.update('process'); + if (this._process !== void 0) { + hash.update(this._process); + } + if (this._args && this._args.length > 0) { + for (let arg of this._args) { + hash.update(arg); + } + } + return hash.digest('hex'); + } } export class ShellExecution implements vscode.ShellExecution { @@ -1411,6 +1427,23 @@ export class ShellExecution implements vscode.ShellExecution { set options(value: vscode.ShellExecutionOptions) { this._options = value; } + + public computeId(): string { + const hash = crypto.createHash('md5'); + hash.update('shell'); + if (this._commandLine !== void 0) { + hash.update(this._commandLine); + } + if (this._command !== void 0) { + hash.update(typeof this._command === 'string' ? this._command : this._command.value); + } + if (this._args && this._args.length > 0) { + for (let arg of this._args) { + hash.update(typeof arg === 'string' ? arg : arg.value); + } + } + return hash.digest('hex'); + } } export enum ShellQuoting { @@ -1485,7 +1518,24 @@ export class Task implements vscode.Task { } private clear(): void { + if (this.__id === void 0) { + return; + } this.__id = undefined; + this._scope = undefined; + this._definitionKey = undefined; + this._definition = undefined; + if (this._execution instanceof ProcessExecution) { + this._definition = { + type: 'process', + id: this._execution.computeId() + }; + } else if (this._execution instanceof ShellExecution) { + this._definition = { + type: 'shell', + id: this._execution.computeId() + }; + } } get definition(): vscode.TaskDefinition { @@ -1770,53 +1820,85 @@ export enum FileChangeType { Deleted = 2 } +export enum FileChangeType2 { + Changed = 1, + Created = 2, + Deleted = 3, +} + export enum FileType { File = 0, Dir = 1, Symlink = 2 } +export class FileSystemError extends Error { + + static EntryExists(message?: string): FileSystemError { + return new FileSystemError(message, 'EntryExists', FileSystemError.EntryExists); + } + static EntryNotFound(message?: string): FileSystemError { + return new FileSystemError(message, 'EntryNotFound', FileSystemError.EntryNotFound); + } + static EntryNotADirectory(message?: string): FileSystemError { + return new FileSystemError(message, 'EntryNotADirectory', FileSystemError.EntryNotADirectory); + } + static EntryIsADirectory(message?: string): FileSystemError { + return new FileSystemError(message, 'EntryIsADirectory', FileSystemError.EntryIsADirectory); + } + + constructor(message?: string, code?: string, hide?: Function) { + super(message); + this.name = code ? `${code} (FileSystemError)` : `FileSystemError`; + + if (typeof Error.captureStackTrace === 'function' && typeof hide === 'function') { + // nice stack traces + Error.captureStackTrace(this, hide); + } + } +} + //#endregion //#region folding api -export class FoldingRangeList { - - ranges: FoldingRange[]; - - constructor(ranges: FoldingRange[]) { - this.ranges = ranges; - } -} - export class FoldingRange { - startLine: number; + start: number; - endLine: number; + end: number; - type?: FoldingRangeType | string; + kind?: FoldingRangeKind; - constructor(startLine: number, endLine: number, type?: FoldingRangeType | string) { - this.startLine = startLine; - this.endLine = endLine; - this.type = type; + constructor(start: number, end: number, kind?: FoldingRangeKind) { + this.start = start; + this.end = end; + this.kind = kind; } } -export enum FoldingRangeType { +export class FoldingRangeKind { /** - * Folding range for a comment + * Kind for folding range representing a comment. The value of the kind is 'comment'. */ - Comment = 'comment', + static readonly Comment = new FoldingRangeKind('comment'); /** - * Folding range for a imports or includes + * Kind for folding range representing a import. The value of the kind is 'imports'. */ - Imports = 'imports', + static readonly Imports = new FoldingRangeKind('imports'); /** - * Folding range for a region (e.g. `#region`) + * Kind for folding range representing regions (for example marked by `#region`, `#endregion`). + * The value of the kind is 'region'. */ - Region = 'region' + static readonly Region = new FoldingRangeKind('region'); + + /** + * Creates a new [FoldingRangeKind](#FoldingRangeKind). + * + * @param value of the kind. + */ + public constructor(public value: string) { + } } //#endregion diff --git a/src/vs/workbench/api/node/extHostUrls.ts b/src/vs/workbench/api/node/extHostUrls.ts new file mode 100644 index 00000000000..c4ba32abf0e --- /dev/null +++ b/src/vs/workbench/api/node/extHostUrls.ts @@ -0,0 +1,46 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import { MainContext, IMainContext, ExtHostUrlsShape, MainThreadUrlsShape } from './extHost.protocol'; +import URI, { UriComponents } from 'vs/base/common/uri'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { toDisposable } from 'vs/base/common/lifecycle'; + +export class ExtHostUrls implements ExtHostUrlsShape { + + private static HandlePool = 0; + private readonly _proxy: MainThreadUrlsShape; + + private handlers = new Map(); + + constructor( + mainContext: IMainContext + ) { + this._proxy = mainContext.getProxy(MainContext.MainThreadUrls); + } + + registerExternalUriHandler(extensionId: string, handler: vscode.ExternalUriHandler): vscode.Disposable { + const handle = ExtHostUrls.HandlePool++; + this.handlers.set(handle, handler); + this._proxy.$registerExternalUriHandler(handle, extensionId); + + return toDisposable(() => { + this.handlers.delete(handle); + this._proxy.$unregisterExternalUriHandler(handle); + }); + } + + $handleExternalUri(handle: number, uri: UriComponents): TPromise { + const handler = this.handlers.get(handle); + + if (!handler) { + return TPromise.as(null); + } + + handler.handleExternalUri(URI.revive(uri)); + return TPromise.as(null); + } +} \ No newline at end of file diff --git a/src/vs/workbench/api/node/extHostWebview.ts b/src/vs/workbench/api/node/extHostWebview.ts index 27f69d0ec1d..e91157377ab 100644 --- a/src/vs/workbench/api/node/extHostWebview.ts +++ b/src/vs/workbench/api/node/extHostWebview.ts @@ -3,73 +3,36 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { MainContext, MainThreadWebviewsShape, IMainContext, ExtHostWebviewsShape, WebviewHandle } from './extHost.protocol'; +import { MainContext, MainThreadWebviewsShape, IMainContext, ExtHostWebviewsShape, WebviewPanelHandle } from './extHost.protocol'; import * as vscode from 'vscode'; import { Event, Emitter } from 'vs/base/common/event'; import * as typeConverters from 'vs/workbench/api/node/extHostTypeConverters'; import { Position } from 'vs/platform/editor/common/editor'; import { TPromise } from 'vs/base/common/winjs.base'; +import { Disposable } from './extHostTypes'; export class ExtHostWebview implements vscode.Webview { - - private readonly _viewType: string; - private _title: string; + private readonly _handle: WebviewPanelHandle; + private readonly _proxy: MainThreadWebviewsShape; private _html: string; private _options: vscode.WebviewOptions; private _isDisposed: boolean = false; - private _viewColumn: vscode.ViewColumn; - private _active: boolean; - public readonly onMessageEmitter = new Emitter(); - public readonly onDidReceiveMessage: Event = this.onMessageEmitter.event; - - public readonly onDisposeEmitter = new Emitter(); - public readonly onDidDispose: Event = this.onDisposeEmitter.event; - - public readonly onDidChangeViewStateEmitter = new Emitter(); - public readonly onDidChangeViewState: Event = this.onDidChangeViewStateEmitter.event; + readonly _onMessageEmitter = new Emitter(); + public readonly onDidReceiveMessage: Event = this._onMessageEmitter.event; constructor( - private readonly _handle: WebviewHandle, - private readonly _proxy: MainThreadWebviewsShape, - viewType: string, - viewColumn: vscode.ViewColumn, + handle: WebviewPanelHandle, + proxy: MainThreadWebviewsShape, options: vscode.WebviewOptions ) { - this._viewType = viewType; - this._viewColumn = viewColumn; + this._handle = handle; + this._proxy = proxy; this._options = options; } - public dispose() { - if (this._isDisposed) { - return; - } - - this._isDisposed = true; - this._proxy.$disposeWebview(this._handle); - - this.onDisposeEmitter.dispose(); - this.onMessageEmitter.dispose(); - this.onDidChangeViewStateEmitter.dispose(); - } - - get viewType(): string { - this.assertNotDisposed(); - return this._viewType; - } - - get title(): string { - this.assertNotDisposed(); - return this._title; - } - - set title(value: string) { - this.assertNotDisposed(); - if (this._title !== value) { - this._title = value; - this._proxy.$setTitle(this._handle, value); - } + dispose() { + this._onMessageEmitter.dispose(); } get html(): string { @@ -90,34 +53,127 @@ export class ExtHostWebview implements vscode.Webview { return this._options; } + public postMessage(message: any): Thenable { + this.assertNotDisposed(); + return this._proxy.$postMessage(this._handle, message); + } + + private assertNotDisposed() { + if (this._isDisposed) { + throw new Error('Webview is disposed'); + } + } +} + +export class ExtHostWebviewPanel implements vscode.WebviewPanel { + + private readonly _handle: WebviewPanelHandle; + private readonly _proxy: MainThreadWebviewsShape; + private readonly _viewType: string; + private _title: string; + + private readonly _options: vscode.WebviewPanelOptions; + private readonly _webview: ExtHostWebview; + private _isDisposed: boolean = false; + private _viewColumn: vscode.ViewColumn; + private _visible: boolean = true; + + readonly _onDisposeEmitter = new Emitter(); + public readonly onDidDispose: Event = this._onDisposeEmitter.event; + + readonly _onDidChangeViewStateEmitter = new Emitter(); + public readonly onDidChangeViewState: Event = this._onDidChangeViewStateEmitter.event; + + + constructor( + handle: WebviewPanelHandle, + proxy: MainThreadWebviewsShape, + viewType: string, + title: string, + viewColumn: vscode.ViewColumn, + editorOptions: vscode.WebviewPanelOptions, + webview: ExtHostWebview + ) { + this._handle = handle; + this._proxy = proxy; + this._viewType = viewType; + this._options = editorOptions; + this._viewColumn = viewColumn; + this._title = title; + this._webview = webview; + } + + public dispose() { + if (this._isDisposed) { + return; + } + + this._isDisposed = true; + this._onDisposeEmitter.fire(); + + this._proxy.$disposeWebview(this._handle); + + this._webview.dispose(); + + this._onDisposeEmitter.dispose(); + this._onDidChangeViewStateEmitter.dispose(); + } + + get webview() { + this.assertNotDisposed(); + return this._webview; + } + + get viewType(): string { + this.assertNotDisposed(); + return this._viewType; + } + + get title(): string { + this.assertNotDisposed(); + return this._title; + } + + set title(value: string) { + this.assertNotDisposed(); + if (this._title !== value) { + this._title = value; + this._proxy.$setTitle(this._handle, value); + } + } + + get options() { + return this._options; + } + get viewColumn(): vscode.ViewColumn { this.assertNotDisposed(); return this._viewColumn; } - get active(): boolean { - this.assertNotDisposed(); - return this._active; - } - - set viewColumn(value: vscode.ViewColumn) { + _setViewColumn(value: vscode.ViewColumn) { this.assertNotDisposed(); this._viewColumn = value; } - set active(value: boolean) { + get visible(): boolean { this.assertNotDisposed(); - this._active = value; + return this._visible; + } + + _setVisible(value: boolean) { + this.assertNotDisposed(); + this._visible = value; } public postMessage(message: any): Thenable { this.assertNotDisposed(); - return this._proxy.$sendMessage(this._handle, message); + return this._proxy.$postMessage(this._handle, message); } - public reveal(viewColumn: vscode.ViewColumn): void { + public reveal(viewColumn?: vscode.ViewColumn): void { this.assertNotDisposed(); - this._proxy.$reveal(this._handle, typeConverters.fromViewColumn(viewColumn)); + this._proxy.$reveal(this._handle, viewColumn ? typeConverters.fromViewColumn(viewColumn) : undefined); } private assertNotDisposed() { @@ -128,13 +184,12 @@ export class ExtHostWebview implements vscode.Webview { } export class ExtHostWebviews implements ExtHostWebviewsShape { - private static handlePool = 1; + private static webviewHandlePool = 1; private readonly _proxy: MainThreadWebviewsShape; - private readonly _webviews = new Map(); - - private _activeWebview: ExtHostWebview | undefined; + private readonly _webviewPanels = new Map(); + private readonly _serializers = new Map(); constructor( mainContext: IMainContext @@ -146,70 +201,99 @@ export class ExtHostWebviews implements ExtHostWebviewsShape { viewType: string, title: string, viewColumn: vscode.ViewColumn, - options: vscode.WebviewOptions, + options: vscode.WebviewPanelOptions & vscode.WebviewOptions, extensionFolderPath: string - ): vscode.Webview { - const handle = ExtHostWebviews.handlePool++; - this._proxy.$createWebview(handle, viewType, title, typeConverters.fromViewColumn(viewColumn), options, extensionFolderPath); + ): vscode.WebviewPanel { + const handle = ExtHostWebviews.webviewHandlePool++ + ''; + this._proxy.$createWebviewPanel(handle, viewType, title, typeConverters.fromViewColumn(viewColumn), options, extensionFolderPath); - const webview = new ExtHostWebview(handle, this._proxy, viewType, viewColumn, options); - this._webviews.set(handle, webview); - return webview; + const webview = new ExtHostWebview(handle, this._proxy, options); + const panel = new ExtHostWebviewPanel(handle, this._proxy, viewType, title, viewColumn, options, webview); + this._webviewPanels.set(handle, panel); + return panel; } - $onMessage(handle: WebviewHandle, message: any): void { - const webview = this.getWebview(handle); - if (webview) { - webview.onMessageEmitter.fire(message); + registerWebviewPanelSerializer( + viewType: string, + serializer: vscode.WebviewPanelSerializer + ): vscode.Disposable { + if (this._serializers.has(viewType)) { + throw new Error(`Serializer for '${viewType}' already registered`); + } + + this._serializers.set(viewType, serializer); + this._proxy.$registerSerializer(viewType); + + return new Disposable(() => { + this._serializers.delete(viewType); + this._proxy.$unregisterSerializer(viewType); + }); + } + + $onMessage(handle: WebviewPanelHandle, message: any): void { + const panel = this.getWebviewPanel(handle); + if (panel) { + panel.webview._onMessageEmitter.fire(message); } } - $onDidChangeActiveWeview(handle: WebviewHandle | undefined): void { - if (handle) { - const webview = this.getWebview(handle); - if (webview) { - if (webview !== this._activeWebview) { - this._activeWebview = webview; - webview.active = true; - webview.onDidChangeViewStateEmitter.fire({ viewColumn: webview.viewColumn, active: true }); - } - } - } else { - if (this._activeWebview) { - this._activeWebview.active = false; - this._activeWebview.onDidChangeViewStateEmitter.fire({ viewColumn: this._activeWebview.viewColumn, active: false }); - this._activeWebview = undefined; + $onDidChangeWebviewPanelViewState(handle: WebviewPanelHandle, visible: boolean, position: Position): void { + const panel = this.getWebviewPanel(handle); + if (panel) { + const viewColumn = typeConverters.toViewColumn(position); + if (panel.visible !== visible || panel.viewColumn !== viewColumn) { + panel._setVisible(visible); + panel._setViewColumn(viewColumn); + panel._onDidChangeViewStateEmitter.fire({ webviewPanel: panel }); } } } - $onDidDisposeWeview(handle: WebviewHandle): Thenable { - const webview = this.getWebview(handle); - if (webview) { - webview.onDisposeEmitter.fire(); - this._webviews.delete(handle); - if (this._activeWebview === webview) { - this._activeWebview = undefined; - } + $onDidDisposeWebviewPanel(handle: WebviewPanelHandle): Thenable { + const panel = this.getWebviewPanel(handle); + if (panel) { + panel.dispose(); + this._webviewPanels.delete(handle); } return TPromise.as(void 0); } - $onDidChangePosition(handle: WebviewHandle, newPosition: Position): void { - const webview = this.getWebview(handle); - if (webview) { - const newViewColumn = typeConverters.toViewColumn(newPosition); - if (webview.viewColumn !== newViewColumn) { - webview.viewColumn = newViewColumn; - webview.onDidChangeViewStateEmitter.fire({ viewColumn: newViewColumn, active: webview.active }); - } + $deserializeWebviewPanel( + webviewHandle: WebviewPanelHandle, + viewType: string, + title: string, + state: any, + position: Position, + options: vscode.WebviewOptions & vscode.WebviewPanelOptions + ): Thenable { + const serializer = this._serializers.get(viewType); + if (!serializer) { + return TPromise.wrapError(new Error(`No serializer found for '${viewType}'`)); } + + const webview = new ExtHostWebview(webviewHandle, this._proxy, options); + const revivedPanel = new ExtHostWebviewPanel(webviewHandle, this._proxy, viewType, title, typeConverters.toViewColumn(position), options, webview); + this._webviewPanels.set(webviewHandle, revivedPanel); + return serializer.deserializeWebviewPanel(revivedPanel, state); } - private readonly _onDidChangeActiveWebview = new Emitter(); - public readonly onDidChangeActiveWebview = this._onDidChangeActiveWebview.event; + $serializeWebviewPanel( + webviewHandle: WebviewPanelHandle + ): Thenable { + const panel = this.getWebviewPanel(webviewHandle); + if (!panel) { + return TPromise.as(undefined); + } - private getWebview(handle: WebviewHandle) { - return this._webviews.get(handle); + const serialzer = this._serializers.get(panel.viewType); + if (!serialzer) { + return TPromise.as(undefined); + } + + return serialzer.serializeWebviewPanel(panel); + } + + private getWebviewPanel(handle: WebviewPanelHandle): ExtHostWebviewPanel | undefined { + return this._webviewPanels.get(handle); } } \ No newline at end of file diff --git a/src/vs/workbench/api/node/extHostWorkspace.ts b/src/vs/workbench/api/node/extHostWorkspace.ts index 88d2d8d3f25..7fd58a461ac 100644 --- a/src/vs/workbench/api/node/extHostWorkspace.ts +++ b/src/vs/workbench/api/node/extHostWorkspace.ts @@ -362,7 +362,7 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape { if (token) { token.onCancellationRequested(() => this._proxy.$cancelSearch(requestId)); } - return result.then(data => data.map(URI.revive)); + return result.then(data => Array.isArray(data) ? data.map(URI.revive) : []); } saveAll(includeUntitled?: boolean): Thenable { diff --git a/src/vs/workbench/api/shared/tasks.ts b/src/vs/workbench/api/shared/tasks.ts index cba95638cbc..7e070957c48 100644 --- a/src/vs/workbench/api/shared/tasks.ts +++ b/src/vs/workbench/api/shared/tasks.ts @@ -11,10 +11,6 @@ export interface TaskDefinitionDTO { [name: string]: any; } -export interface TaskExecutionDTO { - id: string; -} - export interface TaskPresentationOptionsDTO { reveal?: number; echo?: boolean; @@ -85,4 +81,14 @@ export interface TaskDTO { presentationOptions: TaskPresentationOptionsDTO; problemMatchers: string[]; hasDefinedMatchers: boolean; +} + +export interface TaskExecutionDTO { + id: string; + task: TaskDTO; +} + +export interface TaskFilterDTO { + version?: string; + type?: string; } \ No newline at end of file diff --git a/src/vs/workbench/browser/composite.ts b/src/vs/workbench/browser/composite.ts index 0e85d241b4f..545f12395cf 100644 --- a/src/vs/workbench/browser/composite.ts +++ b/src/vs/workbench/browser/composite.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import { TPromise } from 'vs/base/common/winjs.base'; -import { Dimension, Builder } from 'vs/base/browser/builder'; import { IAction, IActionRunner, ActionRunner } from 'vs/base/common/actions'; import { IActionItem } from 'vs/base/browser/ui/actionbar/actionbar'; import { Component } from 'vs/workbench/common/component'; @@ -14,8 +13,8 @@ import { IEditorControl } from 'vs/platform/editor/common/editor'; import { Event, Emitter } from 'vs/base/common/event'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IConstructorSignature0, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import * as DOM from 'vs/base/browser/dom'; import { IDisposable } from 'vs/base/common/lifecycle'; +import { IFocusTracker, trackFocus, Dimension } from 'vs/base/browser/dom'; /** * Composites are layed out in the sidebar and panel part of the workbench. At a time only one composite @@ -31,11 +30,11 @@ export abstract class Composite extends Component implements IComposite { private readonly _onTitleAreaUpdate: Emitter; private readonly _onDidFocus: Emitter; - private _focusTracker?: DOM.IFocusTracker; + private _focusTracker?: IFocusTracker; private _focusListenerDisposable?: IDisposable; private visible: boolean; - private parent: Builder; + private parent: HTMLElement; protected actionRunner: IActionRunner; @@ -75,7 +74,7 @@ export abstract class Composite extends Component implements IComposite { * Note that DOM-dependent calculations should be performed from the setVisible() * call. Only then the composite will be part of the DOM. */ - public create(parent: Builder): TPromise { + public create(parent: HTMLElement): TPromise { this.parent = parent; return TPromise.as(null); @@ -88,12 +87,12 @@ export abstract class Composite extends Component implements IComposite { /** * Returns the container this composite is being build in. */ - public getContainer(): Builder { + public getContainer(): HTMLElement { return this.parent; } public get onDidFocus(): Event { - this._focusTracker = DOM.trackFocus(this.getContainer().getHTMLElement()); + this._focusTracker = trackFocus(this.getContainer()); this._focusListenerDisposable = this._focusTracker.onDidFocus(() => { this._onDidFocus.fire(); }); @@ -243,6 +242,10 @@ export abstract class CompositeDescriptor { } export abstract class CompositeRegistry { + + private readonly _onDidRegister: Emitter> = new Emitter>(); + readonly onDidRegister: Event> = this._onDidRegister.event; + private composites: CompositeDescriptor[]; constructor() { @@ -255,6 +258,7 @@ export abstract class CompositeRegistry { } this.composites.push(descriptor); + this._onDidRegister.fire(descriptor); } public getComposite(id: string): CompositeDescriptor { diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index 0699347dc75..e8dcb1d830b 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -4,11 +4,11 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import { Dimension, Builder } from 'vs/base/browser/builder'; import { TPromise } from 'vs/base/common/winjs.base'; import * as errors from 'vs/base/common/errors'; import { Part } from 'vs/workbench/browser/part'; import { QuickOpenController } from 'vs/workbench/browser/parts/quickopen/quickOpenController'; +import { QuickInputService } from 'vs/workbench/browser/parts/quickinput/quickInput'; import { Sash, ISashEvent, IVerticalSashLayoutProvider, IHorizontalSashLayoutProvider, Orientation } from 'vs/base/browser/ui/sash/sash'; import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IPartService, Position, ILayoutOptions, Parts } from 'vs/workbench/services/part/common/partService'; @@ -22,6 +22,7 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { memoize } from 'vs/base/common/decorators'; import { NotificationsCenter } from 'vs/workbench/browser/parts/notifications/notificationsCenter'; import { NotificationsToasts } from 'vs/workbench/browser/parts/notifications/notificationsToasts'; +import { Dimension, getClientArea, size, position, hide, show } from 'vs/base/browser/dom'; const MIN_SIDEBAR_PART_WIDTH = 170; const DEFAULT_SIDEBAR_PART_WIDTH = 300; @@ -58,8 +59,8 @@ export class WorkbenchLayout implements IVerticalSashLayoutProvider, IHorizontal private static readonly sashYHeightSettingsKey = 'workbench.panel.height'; private static readonly panelSizeBeforeMaximizedKey = 'workbench.panel.sizeBeforeMaximized'; - private parent: Builder; - private workbenchContainer: Builder; + private parent: HTMLElement; + private workbenchContainer: HTMLElement; private titlebar: Part; private activitybar: Part; private editor: Part; @@ -67,6 +68,7 @@ export class WorkbenchLayout implements IVerticalSashLayoutProvider, IHorizontal private panel: Part; private statusbar: Part; private quickopen: QuickOpenController; + private quickInput: QuickInputService; private notificationsCenter: NotificationsCenter; private notificationsToasts: NotificationsToasts; private toUnbind: IDisposable[]; @@ -86,8 +88,8 @@ export class WorkbenchLayout implements IVerticalSashLayoutProvider, IHorizontal // Take parts as an object bag since instatation service does not have typings for constructors with 9+ arguments constructor( - parent: Builder, - workbenchContainer: Builder, + parent: HTMLElement, + workbenchContainer: HTMLElement, parts: { titlebar: Part, activitybar: Part, @@ -97,6 +99,7 @@ export class WorkbenchLayout implements IVerticalSashLayoutProvider, IHorizontal statusbar: Part }, quickopen: QuickOpenController, + quickInput: QuickInputService, notificationsCenter: NotificationsCenter, notificationsToasts: NotificationsToasts, @IStorageService private storageService: IStorageService, @@ -116,21 +119,22 @@ export class WorkbenchLayout implements IVerticalSashLayoutProvider, IHorizontal this.panel = parts.panel; this.statusbar = parts.statusbar; this.quickopen = quickopen; + this.quickInput = quickInput; this.notificationsCenter = notificationsCenter; this.notificationsToasts = notificationsToasts; this.toUnbind = []; this.panelSizeBeforeMaximized = this.storageService.getInteger(WorkbenchLayout.panelSizeBeforeMaximizedKey, StorageScope.GLOBAL, 0); this.panelMaximized = false; - this.sashXOne = new Sash(this.workbenchContainer.getHTMLElement(), this, { + this.sashXOne = new Sash(this.workbenchContainer, this, { baseSize: 5 }); - this.sashXTwo = new Sash(this.workbenchContainer.getHTMLElement(), this, { + this.sashXTwo = new Sash(this.workbenchContainer, this, { baseSize: 5 }); - this.sashY = new Sash(this.workbenchContainer.getHTMLElement(), this, { + this.sashY = new Sash(this.workbenchContainer, this, { baseSize: 4, orientation: Orientation.HORIZONTAL }); @@ -443,7 +447,7 @@ export class WorkbenchLayout implements IVerticalSashLayoutProvider, IHorizontal } public layout(options?: ILayoutOptions): void { - this.workbenchSize = this.parent.getClientArea(); + this.workbenchSize = getClientArea(this.parent); const isActivityBarHidden = !this.partService.isVisible(Parts.ACTIVITYBAR_PART); const isTitlebarHidden = !this.partService.isVisible(Parts.TITLEBAR_PART); @@ -574,12 +578,11 @@ export class WorkbenchLayout implements IVerticalSashLayoutProvider, IHorizontal } // Workbench - this.workbenchContainer - .position(0, 0, 0, 0, 'relative') - .size(this.workbenchSize.width, this.workbenchSize.height); + position(this.workbenchContainer, 0, 0, 0, 0, 'relative'); + size(this.workbenchContainer, this.workbenchSize.width, this.workbenchSize.height); // Bug on Chrome: Sometimes Chrome wants to scroll the workbench container on layout changes. The fix is to reset scrolling in this case. - const workbenchContainer = this.workbenchContainer.getHTMLElement(); + const workbenchContainer = this.workbenchContainer; if (workbenchContainer.scrollTop > 0) { workbenchContainer.scrollTop = 0; } @@ -588,69 +591,78 @@ export class WorkbenchLayout implements IVerticalSashLayoutProvider, IHorizontal } // Title Part + const titleContainer = this.titlebar.getContainer(); if (isTitlebarHidden) { - this.titlebar.getContainer().hide(); + hide(titleContainer); } else { - this.titlebar.getContainer().show(); + show(titleContainer); } // Editor Part and Panel part - this.editor.getContainer().size(editorSize.width, editorSize.height); - this.panel.getContainer().size(panelDimension.width, panelDimension.height); + const editorContainer = this.editor.getContainer(); + const panelContainer = this.panel.getContainer(); + size(editorContainer, editorSize.width, editorSize.height); + size(panelContainer, panelDimension.width, panelDimension.height); if (panelPosition === Position.BOTTOM) { if (sidebarPosition === Position.LEFT) { - this.editor.getContainer().position(this.titlebarHeight, 0, this.statusbarHeight + panelDimension.height, sidebarSize.width + activityBarSize.width); - this.panel.getContainer().position(editorSize.height + this.titlebarHeight, 0, this.statusbarHeight, sidebarSize.width + activityBarSize.width); + position(editorContainer, this.titlebarHeight, 0, this.statusbarHeight + panelDimension.height, sidebarSize.width + activityBarSize.width); + position(panelContainer, editorSize.height + this.titlebarHeight, 0, this.statusbarHeight, sidebarSize.width + activityBarSize.width); } else { - this.editor.getContainer().position(this.titlebarHeight, sidebarSize.width, this.statusbarHeight + panelDimension.height, 0); - this.panel.getContainer().position(editorSize.height + this.titlebarHeight, sidebarSize.width, this.statusbarHeight, 0); + position(editorContainer, this.titlebarHeight, sidebarSize.width, this.statusbarHeight + panelDimension.height, 0); + position(panelContainer, editorSize.height + this.titlebarHeight, sidebarSize.width, this.statusbarHeight, 0); } } else { if (sidebarPosition === Position.LEFT) { - this.editor.getContainer().position(this.titlebarHeight, panelDimension.width, this.statusbarHeight, sidebarSize.width + activityBarSize.width); - this.panel.getContainer().position(this.titlebarHeight, 0, this.statusbarHeight, sidebarSize.width + activityBarSize.width + editorSize.width); + position(editorContainer, this.titlebarHeight, panelDimension.width, this.statusbarHeight, sidebarSize.width + activityBarSize.width); + position(panelContainer, this.titlebarHeight, 0, this.statusbarHeight, sidebarSize.width + activityBarSize.width + editorSize.width); } else { - this.editor.getContainer().position(this.titlebarHeight, sidebarSize.width + activityBarSize.width + panelWidth, this.statusbarHeight, 0); - this.panel.getContainer().position(this.titlebarHeight, sidebarSize.width + activityBarSize.width, this.statusbarHeight, editorSize.width); + position(editorContainer, this.titlebarHeight, sidebarSize.width + activityBarSize.width + panelWidth, this.statusbarHeight, 0); + position(panelContainer, this.titlebarHeight, sidebarSize.width + activityBarSize.width, this.statusbarHeight, editorSize.width); } } // Activity Bar Part - this.activitybar.getContainer().size(null, activityBarSize.height); + const activitybarContainer = this.activitybar.getContainer(); + size(activitybarContainer, null, activityBarSize.height); if (sidebarPosition === Position.LEFT) { - this.activitybar.getContainer().getHTMLElement().style.right = ''; - this.activitybar.getContainer().position(this.titlebarHeight, null, 0, 0); + this.activitybar.getContainer().style.right = ''; + position(activitybarContainer, this.titlebarHeight, null, 0, 0); } else { - this.activitybar.getContainer().getHTMLElement().style.left = ''; - this.activitybar.getContainer().position(this.titlebarHeight, 0, 0, null); + this.activitybar.getContainer().style.left = ''; + position(activitybarContainer, this.titlebarHeight, 0, 0, null); } if (isActivityBarHidden) { - this.activitybar.getContainer().hide(); + hide(activitybarContainer); } else { - this.activitybar.getContainer().show(); + show(activitybarContainer); } // Sidebar Part - this.sidebar.getContainer().size(sidebarSize.width, sidebarSize.height); + const sidebarContainer = this.sidebar.getContainer(); + size(sidebarContainer, sidebarSize.width, sidebarSize.height); const editorAndPanelWidth = editorSize.width + (panelPosition === Position.RIGHT ? panelWidth : 0); if (sidebarPosition === Position.LEFT) { - this.sidebar.getContainer().position(this.titlebarHeight, editorAndPanelWidth, this.statusbarHeight, activityBarSize.width); + position(sidebarContainer, this.titlebarHeight, editorAndPanelWidth, this.statusbarHeight, activityBarSize.width); } else { - this.sidebar.getContainer().position(this.titlebarHeight, activityBarSize.width, this.statusbarHeight, editorAndPanelWidth); + position(sidebarContainer, this.titlebarHeight, activityBarSize.width, this.statusbarHeight, editorAndPanelWidth); } // Statusbar Part - this.statusbar.getContainer().position(this.workbenchSize.height - this.statusbarHeight); + const statusbarContainer = this.statusbar.getContainer(); + position(statusbarContainer, this.workbenchSize.height - this.statusbarHeight); if (isStatusbarHidden) { - this.statusbar.getContainer().hide(); + hide(statusbarContainer); } else { - this.statusbar.getContainer().show(); + show(statusbarContainer); } // Quick open this.quickopen.layout(this.workbenchSize); + // Quick input + this.quickInput.layout(this.workbenchSize); + // Notifications this.notificationsCenter.layout(this.workbenchSize); this.notificationsToasts.layout(this.workbenchSize); diff --git a/src/vs/workbench/browser/panel.ts b/src/vs/workbench/browser/panel.ts index a44f1f6c2e2..8b05f420b0b 100644 --- a/src/vs/workbench/browser/panel.ts +++ b/src/vs/workbench/browser/panel.ts @@ -95,7 +95,7 @@ export abstract class TogglePanelAction extends Action { const activePanel = this.panelService.getActivePanel(); const activeElement = document.activeElement; - return activePanel && activeElement && DOM.isAncestor(activeElement, (activePanel).getContainer().getHTMLElement()); + return activePanel && activeElement && DOM.isAncestor(activeElement, (activePanel).getContainer()); } } diff --git a/src/vs/workbench/browser/part.ts b/src/vs/workbench/browser/part.ts index 007441d57dc..8f147ff99bc 100644 --- a/src/vs/workbench/browser/part.ts +++ b/src/vs/workbench/browser/part.ts @@ -6,9 +6,9 @@ 'use strict'; import 'vs/css!./media/part'; -import { Dimension, Builder } from 'vs/base/browser/builder'; import { Component } from 'vs/workbench/common/component'; import { IThemeService, ITheme } from 'vs/platform/theme/common/themeService'; +import { Dimension, size } from 'vs/base/browser/dom'; export interface IPartOptions { hasTitle?: boolean; @@ -20,9 +20,9 @@ export interface IPartOptions { * and mandatory content area to show content. */ export abstract class Part extends Component { - private parent: Builder; - private titleArea: Builder; - private contentArea: Builder; + private parent: HTMLElement; + private titleArea: HTMLElement; + private contentArea: HTMLElement; private partLayout: PartLayout; constructor( @@ -47,7 +47,7 @@ export abstract class Part extends Component { * * Called to create title and content area of the part. */ - public create(parent: Builder): void { + public create(parent: HTMLElement): void { this.parent = parent; this.titleArea = this.createTitleArea(parent); this.contentArea = this.createContentArea(parent); @@ -60,35 +60,35 @@ export abstract class Part extends Component { /** * Returns the overall part container. */ - public getContainer(): Builder { + public getContainer(): HTMLElement { return this.parent; } /** * Subclasses override to provide a title area implementation. */ - protected createTitleArea(parent: Builder): Builder { + protected createTitleArea(parent: HTMLElement): HTMLElement { return null; } /** * Returns the title area container. */ - protected getTitleArea(): Builder { + protected getTitleArea(): HTMLElement { return this.titleArea; } /** * Subclasses override to provide a content area implementation. */ - protected createContentArea(parent: Builder): Builder { + protected createContentArea(parent: HTMLElement): HTMLElement { return null; } /** * Returns the content area container. */ - protected getContentArea(): Builder { + protected getContentArea(): HTMLElement { return this.contentArea; } @@ -104,8 +104,7 @@ const TITLE_HEIGHT = 35; export class PartLayout { - constructor(container: Builder, private options: IPartOptions, titleArea: Builder, private contentArea: Builder) { - } + constructor(container: HTMLElement, private options: IPartOptions, titleArea: HTMLElement, private contentArea: HTMLElement) { } public layout(dimension: Dimension): Dimension[] { const { width, height } = dimension; @@ -133,7 +132,7 @@ export class PartLayout { // Content if (this.contentArea) { - this.contentArea.size(contentSize.width, contentSize.height); + size(this.contentArea, contentSize.width, contentSize.height); } return sizes; diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts index 6a68273d4c7..d63f117ff44 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts @@ -9,7 +9,7 @@ import 'vs/css!./media/activitybarpart'; import * as nls from 'vs/nls'; import { TPromise } from 'vs/base/common/winjs.base'; import { illegalArgument } from 'vs/base/common/errors'; -import { Builder, $, Dimension } from 'vs/base/browser/builder'; +import { $ } from 'vs/base/browser/builder'; import { Action } from 'vs/base/common/actions'; import { ActionsOrientation, ActionBar, Separator } from 'vs/base/browser/ui/actionbar/actionbar'; import { GlobalActivityExtensions, IGlobalActivityRegistry } from 'vs/workbench/common/activity'; @@ -29,6 +29,9 @@ import { ACTIVITY_BAR_BACKGROUND, ACTIVITY_BAR_BORDER, ACTIVITY_BAR_FOREGROUND, import { contrastBorder } from 'vs/platform/theme/common/colorRegistry'; import { CompositeBar } from 'vs/workbench/browser/parts/compositebar/compositeBar'; import { ToggleCompositePinnedAction } from 'vs/workbench/browser/parts/compositebar/compositeBarActions'; +import { ViewLocation, ViewsRegistry } from 'vs/workbench/common/views'; +import { ViewletDescriptor } from 'vs/workbench/browser/viewlet'; +import { Dimension } from 'vs/base/browser/dom'; export class ActivitybarPart extends Part { @@ -61,6 +64,7 @@ export class ActivitybarPart extends Part { super(id, { hasTitle: false }, themeService); this.globalActivityIdToActions = Object.create(null); + this.compositeBar = this.instantiationService.createInstance(CompositeBar, { icon: true, storageId: ActivitybarPart.PINNED_VIEWLETS, @@ -75,11 +79,17 @@ export class ActivitybarPart extends Part { colors: ActivitybarPart.COLORS, overflowActionSize: ActivitybarPart.ACTION_HEIGHT }); + this.registerListeners(); + this.updateCompositebar(); } private registerListeners(): void { + this.toUnbind.push(this.viewletService.onDidViewletRegister(() => this.updateCompositebar())); + this.toUnbind.push(ViewsRegistry.onViewsRegistered(() => this.updateCompositebar())); + this.toUnbind.push(ViewsRegistry.onViewsDeregistered(() => this.updateCompositebar())); + // Activate viewlet action on opening of a viewlet this.toUnbind.push(this.viewletService.onDidViewletOpen(viewlet => this.compositeBar.activateComposite(viewlet.getId()))); @@ -88,7 +98,7 @@ export class ActivitybarPart extends Part { this.toUnbind.push(this.compositeBar.onDidContextMenu(e => this.showContextMenu(e))); this.toUnbind.push(this.viewletService.onDidViewletEnablementChange(({ id, enabled }) => { if (enabled) { - this.compositeBar.addComposite(this.viewletService.getViewlet(id)); + this.compositeBar.addComposite(this.viewletService.getViewlet(id), true); } else { this.compositeBar.removeComposite(id); } @@ -118,7 +128,7 @@ export class ActivitybarPart extends Part { return toDisposable(() => action.setBadge(undefined)); } - public createContentArea(parent: Builder): Builder { + public createContentArea(parent: HTMLElement): HTMLElement { const $el = $(parent); const $result = $('.content').appendTo($el); @@ -128,14 +138,14 @@ export class ActivitybarPart extends Part { // Top Actionbar with action items for each viewlet action this.createGlobalActivityActionBar($('.global-activity').appendTo($result).getHTMLElement()); - return $result; + return $result.getHTMLElement(); } public updateStyles(): void { super.updateStyles(); // Part container - const container = this.getContainer(); + const container = $(this.getContainer()); const background = this.getColor(ACTIVITY_BAR_BACKGROUND); container.style('background-color', background); @@ -153,7 +163,9 @@ export class ActivitybarPart extends Part { private showContextMenu(e: MouseEvent): void { const event = new StandardMouseEvent(e); - const actions: Action[] = this.viewletService.getViewlets().map(viewlet => this.instantiationService.createInstance(ToggleCompositePinnedAction, viewlet, this.compositeBar)); + const actions: Action[] = this.viewletService.getViewlets() + .filter(viewlet => this.canShow(viewlet)) + .map(viewlet => this.instantiationService.createInstance(ToggleCompositePinnedAction, viewlet, this.compositeBar)); actions.push(new Separator()); actions.push(this.instantiationService.createInstance(ToggleActivityBarVisibilityAction, ToggleActivityBarVisibilityAction.ID, nls.localize('hideActivitBar', "Hide Activity Bar"))); @@ -185,6 +197,26 @@ export class ActivitybarPart extends Part { }); } + private updateCompositebar(): void { + const viewlets = this.viewletService.getViewlets(); + for (const viewlet of viewlets) { + const canShow = this.canShow(viewlet); + if (canShow) { + this.compositeBar.addComposite(viewlet, false); + } else { + this.compositeBar.removeComposite(viewlet.id); + } + } + } + + private canShow(viewlet: ViewletDescriptor): boolean { + const viewLocation = ViewLocation.get(viewlet.id); + if (viewLocation) { + return ViewsRegistry.getViews(viewLocation).length > 0; + } + return true; + } + public getPinned(): string[] { return this.viewletService.getViewlets().map(v => v.id).filter(id => this.compositeBar.isPinned(id)); } @@ -212,6 +244,11 @@ export class ActivitybarPart extends Part { return sizes; } + public shutdown(): void { + this.compositeBar.shutdown(); + super.shutdown(); + } + public dispose(): void { if (this.compositeBar) { this.compositeBar.dispose(); @@ -225,4 +262,4 @@ export class ActivitybarPart extends Part { super.dispose(); } -} +} \ No newline at end of file diff --git a/src/vs/workbench/browser/parts/compositePart.ts b/src/vs/workbench/browser/parts/compositePart.ts index a209aeada4f..4111789e023 100644 --- a/src/vs/workbench/browser/parts/compositePart.ts +++ b/src/vs/workbench/browser/parts/compositePart.ts @@ -10,18 +10,16 @@ import * as nls from 'vs/nls'; import { defaultGenerator } from 'vs/base/common/idGenerator'; import { TPromise } from 'vs/base/common/winjs.base'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { Dimension, Builder, $ } from 'vs/base/browser/builder'; +import { Builder, $ } from 'vs/base/browser/builder'; import * as strings from 'vs/base/common/strings'; import { Emitter } from 'vs/base/common/event'; import * as types from 'vs/base/common/types'; import * as errors from 'vs/base/common/errors'; -import * as DOM from 'vs/base/browser/dom'; -import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; import { IActionItem, ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar'; import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar'; import { prepareActions } from 'vs/workbench/browser/actions'; -import { Action, IAction, IRunEvent } from 'vs/base/common/actions'; +import { Action, IAction } from 'vs/base/common/actions'; import { Part, IPartOptions } from 'vs/workbench/browser/part'; import { Composite, CompositeRegistry } from 'vs/workbench/browser/composite'; import { IComposite } from 'vs/workbench/common/composite'; @@ -37,6 +35,7 @@ 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 { INotificationService } from 'vs/platform/notification/common/notification'; +import { Dimension } from 'vs/base/browser/dom'; export interface ICompositeTitleLabel { @@ -74,10 +73,10 @@ export abstract class CompositePart extends Part { private telemetryService: ITelemetryService, protected contextMenuService: IContextMenuService, protected partService: IPartService, - private keybindingService: IKeybindingService, + protected keybindingService: IKeybindingService, protected instantiationService: IInstantiationService, themeService: IThemeService, - private registry: CompositeRegistry, + protected readonly registry: CompositeRegistry, private activeCompositeSettingsKey: string, private defaultCompositeId: string, private nameForTelemetry: string, @@ -223,8 +222,8 @@ export abstract class CompositePart extends Part { compositeContainer = $().div({ 'class': ['composite', this.compositeCSSClass], id: composite.getId() - }, (div: Builder) => { - createCompositePromise = composite.create(div).then(() => { + }, div => { + createCompositePromise = composite.create(div.getHTMLElement()).then(() => { composite.updateStyles(); }); }); @@ -279,7 +278,7 @@ export abstract class CompositePart extends Part { } // Action Run Handling - this.telemetryActionsListener = this.toolBar.actionRunner.onDidRun((e: IRunEvent) => { + this.telemetryActionsListener = this.toolBar.actionRunner.onDidRun(e => { // Check for Error if (e.error && !errors.isPromiseCanceledError(e.error)) { @@ -311,7 +310,7 @@ export abstract class CompositePart extends Part { composite.layout(this.contentAreaSize); } }); - }, (error: any) => this.onError(error)); + }, error => this.onError(error)); } protected onTitleAreaUpdate(compositeId: string): void { @@ -391,7 +390,7 @@ export abstract class CompositePart extends Part { compositeContainer.hide(); // Clear any running Progress - this.progressBar.stop().getContainer().hide(); + this.progressBar.stop().hide(); // Empty Actions this.toolBar.setActions([])(); @@ -401,39 +400,37 @@ export abstract class CompositePart extends Part { }); } - public createTitleArea(parent: Builder): Builder { + public createTitleArea(parent: HTMLElement): HTMLElement { // Title Area Container const titleArea = $(parent).div({ 'class': ['composite', 'title'] }); - $(titleArea).on(DOM.EventType.CONTEXT_MENU, (e: MouseEvent) => this.onTitleAreaContextMenu(new StandardMouseEvent(e))); - // Left Title Label - this.titleLabel = this.createTitleLabel(titleArea); + this.titleLabel = this.createTitleLabel(titleArea.getHTMLElement()); // Right Actions Container $(titleArea).div({ 'class': 'title-actions' - }, (div) => { + }, div => { // Toolbar this.toolBar = new ToolBar(div.getHTMLElement(), this.contextMenuService, { - actionItemProvider: (action: Action) => this.actionItemProvider(action), + actionItemProvider: action => this.actionItemProvider(action as Action), orientation: ActionsOrientation.HORIZONTAL, - getKeyBinding: (action) => this.keybindingService.lookupKeybinding(action.id) + getKeyBinding: action => this.keybindingService.lookupKeybinding(action.id) }); }); - return titleArea; + return titleArea.getHTMLElement(); } - protected createTitleLabel(parent: Builder): ICompositeTitleLabel { + protected createTitleLabel(parent: HTMLElement): ICompositeTitleLabel { let titleLabel: Builder; $(parent).div({ 'class': 'title-label' - }, (div) => { + }, div => { titleLabel = div.span(); }); @@ -456,27 +453,8 @@ export abstract class CompositePart extends Part { this.titleLabel.updateStyles(); } - private onTitleAreaContextMenu(event: StandardMouseEvent): void { - if (this.activeComposite) { - const contextMenuActions = this.getTitleAreaContextMenuActions(); - if (contextMenuActions.length) { - const anchor: { x: number, y: number } = { x: event.posx, y: event.posy }; - this.contextMenuService.showContextMenu({ - getAnchor: () => anchor, - getActions: () => TPromise.as(contextMenuActions), - getActionItem: (action: Action) => this.actionItemProvider(action), - actionRunner: this.activeComposite.getActionRunner(), - getKeyBinding: (action) => this.keybindingService.lookupKeybinding(action.id) - }); - } - } - } + protected actionItemProvider(action: Action): IActionItem { - protected getTitleAreaContextMenuActions(): IAction[] { - return this.activeComposite ? this.activeComposite.getContextMenuActions() : []; - } - - private actionItemProvider(action: Action): IActionItem { // Check Active Composite if (this.activeComposite) { return this.activeComposite.getActionItem(action); @@ -485,14 +463,14 @@ export abstract class CompositePart extends Part { return undefined; } - public createContentArea(parent: Builder): Builder { + public createContentArea(parent: HTMLElement): HTMLElement { return $(parent).div({ 'class': 'content' - }, (div: Builder) => { - this.progressBar = new ProgressBar(div); + }, div => { + this.progressBar = new ProgressBar(div.getHTMLElement()); this.toUnbind.push(attachProgressBarStyler(this.progressBar, this.themeService)); - this.progressBar.getContainer().hide(); - }); + this.progressBar.hide(); + }).getHTMLElement(); } private onError(error: any): void { diff --git a/src/vs/workbench/browser/parts/compositebar/compositeBar.ts b/src/vs/workbench/browser/parts/compositebar/compositeBar.ts index 08dc54643ee..ac68fe3ff8e 100644 --- a/src/vs/workbench/browser/parts/compositebar/compositeBar.ts +++ b/src/vs/workbench/browser/parts/compositebar/compositeBar.ts @@ -8,9 +8,7 @@ import * as nls from 'vs/nls'; import { Action } from 'vs/base/common/actions'; import { illegalArgument } from 'vs/base/common/errors'; -import * as dom from 'vs/base/browser/dom'; import * as arrays from 'vs/base/common/arrays'; -import { Dimension } from 'vs/base/browser/builder'; import { dispose, IDisposable } from 'vs/base/common/lifecycle'; import { IBadge } from 'vs/workbench/services/activity/common/activity'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; @@ -19,6 +17,7 @@ import { ActionBar, IActionItem, ActionsOrientation } from 'vs/base/browser/ui/a import { Event, Emitter } from 'vs/base/common/event'; import { CompositeActionItem, CompositeOverflowActivityAction, ICompositeActivity, CompositeOverflowActivityActionItem, ActivityAction, ICompositeBar, ICompositeBarColors } from 'vs/workbench/browser/parts/compositebar/compositeBarActions'; import { TPromise } from 'vs/base/common/winjs.base'; +import { Dimension, $, addDisposableListener, EventType, EventHelper } from 'vs/base/browser/dom'; export interface ICompositeBarOptions { icon: boolean; @@ -35,6 +34,11 @@ export interface ICompositeBarOptions { hidePart: () => TPromise; } +interface CompositeState { + id: string; + pinned: boolean; +} + export class CompositeBar implements ICompositeBar { private readonly _onDidContextMenu: Emitter; @@ -51,6 +55,7 @@ export class CompositeBar implements ICompositeBar { private compositeIdToActivityStack: { [compositeId: string]: ICompositeActivity[]; }; private compositeSizeInBar: Map; + private initialCompositesStates: CompositeState[]; private pinnedComposites: string[]; private activeCompositeId: string; private activeUnpinnedCompositeId: string; @@ -67,30 +72,36 @@ export class CompositeBar implements ICompositeBar { this.compositeSizeInBar = new Map(); this._onDidContextMenu = new Emitter(); - - const pinnedComposites = JSON.parse(this.storageService.get(this.options.storageId, StorageScope.GLOBAL, null)) as string[]; - if (pinnedComposites) { - const compositeIds = this.options.composites.map(c => c.id); - this.pinnedComposites = pinnedComposites.filter(pcid => compositeIds.indexOf(pcid) >= 0); - } else { - this.pinnedComposites = this.options.composites.map(c => c.id); - } + this.initialCompositesStates = this.loadCompositesStates(); + this.pinnedComposites = this.initialCompositesStates + .filter(c => c.pinned) + .map(c => c.id) + .filter(id => this.options.composites.some(c => c.id === id)); } public get onDidContextMenu(): Event { return this._onDidContextMenu.event; } - public addComposite(compositeData: { id: string; name: string, order: number }): void { + public addComposite(compositeData: { id: string; name: string, order: number }, activate: boolean): void { if (this.options.composites.filter(c => c.id === compositeData.id).length) { return; } - let i = 0; - while (i < this.options.composites.length && this.options.composites[i].order < compositeData.order) { - i++; - } this.options.composites.push(compositeData); - this.pin(compositeData.id, true, i); + + const compositeState = this.initialCompositesStates.filter(c => c.id === compositeData.id)[0]; + if (!compositeState /* new composites are pinned by default */ || compositeState.pinned) { + let index; + if (compositeState) { + index = this.initialCompositesStates.indexOf(compositeState); + } else { + index = 0; + while (index < this.options.composites.length && this.options.composites[index].order < compositeData.order) { + index++; + } + } + this.pin(compositeData.id, true, index, activate); + } } public removeComposite(id: string): void { @@ -101,6 +112,8 @@ export class CompositeBar implements ICompositeBar { this.options.composites = this.options.composites.filter(c => c.id !== id); this.unpin(id); this.pullComposite(id); + // Only at the end deactivate composite so the unpin and pull properly finish + this.deactivateComposite(id); } public activateComposite(id: string): void { @@ -123,6 +136,12 @@ export class CompositeBar implements ICompositeBar { if (this.compositeIdToActions[id]) { this.compositeIdToActions[id].deactivate(); } + if (this.activeCompositeId === id) { + this.activeCompositeId = undefined; + } + if (this.activeUnpinnedCompositeId === id) { + this.activeUnpinnedCompositeId = undefined; + } } public showActivity(compositeId: string, badge: IBadge, clazz?: string, priority?: number): IDisposable { @@ -195,7 +214,7 @@ export class CompositeBar implements ICompositeBar { } public create(parent: HTMLElement): HTMLElement { - const actionBarDiv = parent.appendChild(dom.$('.composite-bar')); + const actionBarDiv = parent.appendChild($('.composite-bar')); this.compositeSwitcherBar = new ActionBar(actionBarDiv, { actionItemProvider: (action: Action) => action instanceof CompositeOverflowActivityAction ? this.compositeOverflowActionItem : this.compositeIdToActionItems[action.id], orientation: this.options.orientation, @@ -205,16 +224,16 @@ export class CompositeBar implements ICompositeBar { this.toDispose.push(this.compositeSwitcherBar); // Contextmenu for composites - this.toDispose.push(dom.addDisposableListener(parent, dom.EventType.CONTEXT_MENU, (e: MouseEvent) => { - dom.EventHelper.stop(e, true); + this.toDispose.push(addDisposableListener(parent, EventType.CONTEXT_MENU, (e: MouseEvent) => { + EventHelper.stop(e, true); this._onDidContextMenu.fire(e); })); // Allow to drop at the end to move composites to the end - this.toDispose.push(dom.addDisposableListener(parent, dom.EventType.DROP, (e: DragEvent) => { + this.toDispose.push(addDisposableListener(parent, EventType.DROP, (e: DragEvent) => { const draggedCompositeId = CompositeActionItem.getDraggedCompositeId(); if (draggedCompositeId) { - dom.EventHelper.stop(e, true); + EventHelper.stop(e, true); CompositeActionItem.clearDraggedComposite(); const targetId = this.pinnedComposites[this.pinnedComposites.length - 1]; @@ -428,19 +447,20 @@ export class CompositeBar implements ICompositeBar { }); // Persist - this.savePinnedComposites(); + this.saveCompositesStates(); } public isPinned(compositeId: string): boolean { return this.pinnedComposites.indexOf(compositeId) >= 0; } - public pin(compositeId: string, update = true, index = this.pinnedComposites.length): void { + public pin(compositeId: string, update = true, index = this.pinnedComposites.length, activate: boolean = true): void { if (this.isPinned(compositeId)) { return; } - this.options.openComposite(compositeId).then(() => { + const activatePromise = activate ? this.options.openComposite(compositeId) : TPromise.as(null); + activatePromise.then(() => { this.pinnedComposites.splice(index, 0, compositeId); this.pinnedComposites = arrays.distinct(this.pinnedComposites); @@ -449,7 +469,7 @@ export class CompositeBar implements ICompositeBar { } // Persist - this.savePinnedComposites(); + this.saveCompositesStates(); }); } @@ -481,7 +501,7 @@ export class CompositeBar implements ICompositeBar { }, 0); // Persist - this.savePinnedComposites(); + this.saveCompositesStates(); } public layout(dimension: Dimension): void { @@ -505,8 +525,33 @@ export class CompositeBar implements ICompositeBar { this.updateCompositeSwitcher(); } - private savePinnedComposites(): void { - this.storageService.store(this.options.storageId, JSON.stringify(this.pinnedComposites), StorageScope.GLOBAL); + private loadCompositesStates(): CompositeState[] { + const storedStates = >JSON.parse(this.storageService.get(this.options.storageId, StorageScope.GLOBAL, '[]')); + const isOldData = storedStates && storedStates.length && typeof storedStates[0] === 'string'; + const compositeStates = storedStates.map(c => + typeof c === 'string' /* migration from pinned states to composites states */ ? { id: c, pinned: true } : c); + + if (!isOldData) { /* Add new composites only if it is new data */ + const newComposites = this.options.composites.filter(c => compositeStates.every(s => s.id !== c.id)); + newComposites.sort((c1, c2) => c1.order < c2.order ? -1 : 1); + newComposites.forEach(c => compositeStates.push({ id: c.id, pinned: true /* new composites are pinned by default */ })); + } + + return compositeStates; + } + + private saveCompositesStates(): void { + const toSave = this.pinnedComposites.map(id => ({ id, pinned: true })); + for (const composite of this.options.composites) { + if (this.pinnedComposites.indexOf(composite.id) === -1) { // Unpinned composites + toSave.push({ id: composite.id, pinned: false }); + } + } + this.storageService.store(this.options.storageId, JSON.stringify(toSave), StorageScope.GLOBAL); + } + + public shutdown(): void { + this.saveCompositesStates(); } public dispose(): void { diff --git a/src/vs/workbench/browser/parts/compositebar/compositeBarActions.ts b/src/vs/workbench/browser/parts/compositebar/compositeBarActions.ts index 6fb75715ca3..13ceb5abbbf 100644 --- a/src/vs/workbench/browser/parts/compositebar/compositeBarActions.ts +++ b/src/vs/workbench/browser/parts/compositebar/compositeBarActions.ts @@ -193,7 +193,7 @@ export class ActivityActionItem extends BaseActionItem { this.$label.text(this.getAction().label); } - this.$badge = this.builder.clone().div({ 'class': 'badge' }, (badge: Builder) => { + this.$badge = this.builder.clone().div({ 'class': 'badge' }, badge => { this.$badgeContent = badge.div({ 'class': 'badge-content' }); }); diff --git a/src/vs/workbench/browser/parts/editor/baseEditor.ts b/src/vs/workbench/browser/parts/editor/baseEditor.ts index b38ca7a0638..2a8e0b70b56 100644 --- a/src/vs/workbench/browser/parts/editor/baseEditor.ts +++ b/src/vs/workbench/browser/parts/editor/baseEditor.ts @@ -5,7 +5,6 @@ 'use strict'; import { TPromise } from 'vs/base/common/winjs.base'; -import { Builder } from 'vs/base/browser/builder'; import { Panel } from 'vs/workbench/browser/panel'; import { EditorInput, EditorOptions } from 'vs/workbench/common/editor'; import { IEditor, Position } from 'vs/platform/editor/common/editor'; @@ -64,9 +63,9 @@ export abstract class BaseEditor extends Panel implements IEditor { this._options = null; } - public create(parent: Builder): void; // create is sync for editors - public create(parent: Builder): TPromise; - public create(parent: Builder): TPromise { + public create(parent: HTMLElement): void; // create is sync for editors + public create(parent: HTMLElement): TPromise; + public create(parent: HTMLElement): TPromise { const res = super.create(parent); // Create Editor @@ -76,9 +75,16 @@ export abstract class BaseEditor extends Panel implements IEditor { } /** - * Called to create the editor in the parent builder. + * Called to create the editor in the parent HTMLElement. */ - protected abstract createEditor(parent: Builder): void; + protected abstract createEditor(parent: HTMLElement): void; + + /** + * Subclasses can set this to false if it does not make sense to center editor input. + */ + public supportsCenteredLayout(): boolean { + return true; + } /** * Overload this function to allow for passing in a position argument. @@ -119,4 +125,4 @@ export abstract class BaseEditor extends Panel implements IEditor { // Super Dispose super.dispose(); } -} \ No newline at end of file +} diff --git a/src/vs/workbench/browser/parts/editor/binaryEditor.ts b/src/vs/workbench/browser/parts/editor/binaryEditor.ts index 28e2795592b..f4df65c25c7 100644 --- a/src/vs/workbench/browser/parts/editor/binaryEditor.ts +++ b/src/vs/workbench/browser/parts/editor/binaryEditor.ts @@ -7,39 +7,49 @@ import * as nls from 'vs/nls'; import { Event, Emitter } from 'vs/base/common/event'; -import URI from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; -import { Dimension, Builder, $ } from 'vs/base/browser/builder'; -import { EditorModel, EditorInput, EditorOptions } from 'vs/workbench/common/editor'; +import { Builder, $ } from 'vs/base/browser/builder'; +import { EditorInput, EditorOptions } from 'vs/workbench/common/editor'; import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; import { BinaryEditorModel } from 'vs/workbench/common/editor/binaryEditorModel'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; import { ScrollbarVisibility } from 'vs/base/common/scrollable'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { IWindowsService } from 'vs/platform/windows/common/windows'; import { ResourceViewerContext, ResourceViewer } from 'vs/workbench/browser/parts/editor/resourceViewer'; +import URI from 'vs/base/common/uri'; +import { Dimension } from 'vs/base/browser/dom'; + +export interface IOpenCallbacks { + openInternal: (input: EditorInput, options: EditorOptions) => void; + openExternal: (uri: URI) => void; +} /* * This class is only intended to be subclassed and not instantiated. */ export abstract class BaseBinaryResourceEditor extends BaseEditor { - private readonly _onMetadataChanged: Emitter; - private metadata: string; + private readonly _onMetadataChanged: Emitter; + + private callbacks: IOpenCallbacks; + private metadata: string; private binaryContainer: Builder; private scrollbar: DomScrollableElement; private resourceViewerContext: ResourceViewerContext; constructor( id: string, + callbacks: IOpenCallbacks, telemetryService: ITelemetryService, - themeService: IThemeService, - private windowsService: IWindowsService + themeService: IThemeService ) { super(id, telemetryService, themeService); this._onMetadataChanged = new Emitter(); + this.toUnbind.push(this._onMetadataChanged); + + this.callbacks = callbacks; } public get onMetadataChanged(): Event { @@ -50,7 +60,7 @@ export abstract class BaseBinaryResourceEditor extends BaseEditor { return this.input ? this.input.getName() : nls.localize('binaryEditor', "Binary Viewer"); } - protected createEditor(parent: Builder): void { + protected createEditor(parent: HTMLElement): void { // Container for Binary const binaryContainerElement = document.createElement('div'); @@ -61,7 +71,7 @@ export abstract class BaseBinaryResourceEditor extends BaseEditor { // Custom Scrollbars this.scrollbar = new DomScrollableElement(binaryContainerElement, { horizontal: ScrollbarVisibility.Auto, vertical: ScrollbarVisibility.Auto }); - parent.getHTMLElement().appendChild(this.scrollbar.getDomNode()); + parent.appendChild(this.scrollbar.getDomNode()); } public setInput(input: EditorInput, options?: EditorOptions): TPromise { @@ -74,10 +84,10 @@ export abstract class BaseBinaryResourceEditor extends BaseEditor { // Otherwise set input and resolve return super.setInput(input, options).then(() => { - return input.resolve(true).then((resolvedModel: EditorModel) => { + return input.resolve(true).then(model => { // Assert Model instance - if (!(resolvedModel instanceof BinaryEditorModel)) { + if (!(model instanceof BinaryEditorModel)) { return TPromise.wrapError(new Error('Unable to open file as binary')); } @@ -87,21 +97,14 @@ export abstract class BaseBinaryResourceEditor extends BaseEditor { } // Render Input - const model = resolvedModel; this.resourceViewerContext = ResourceViewer.show( { name: model.getName(), resource: model.getResource(), size: model.getSize(), etag: model.getETag(), mime: model.getMime() }, - this.binaryContainer, + this.binaryContainer.getHTMLElement(), this.scrollbar, - (resource: URI) => { - this.windowsService.openExternal(resource.toString()).then(didOpen => { - if (!didOpen) { - return this.windowsService.showItemInFolder(resource.fsPath); - } - - return void 0; - }); - }, - (meta) => this.handleMetadataChanged(meta)); + resource => this.callbacks.openInternal(input, options), + resource => this.callbacks.openExternal(resource), + meta => this.handleMetadataChanged(meta) + ); return TPromise.as(null); }); @@ -117,6 +120,10 @@ export abstract class BaseBinaryResourceEditor extends BaseEditor { return this.metadata; } + public supportsCenteredLayout(): boolean { + return false; + } + public clearInput(): void { // Clear Meta diff --git a/src/vs/workbench/browser/parts/editor/editorGroupsControl.ts b/src/vs/workbench/browser/parts/editor/editorGroupsControl.ts index 87fea4fca7f..b67c3c3b42f 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupsControl.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupsControl.ts @@ -10,7 +10,7 @@ import * as arrays from 'vs/base/common/arrays'; import { Event, Emitter } from 'vs/base/common/event'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import * as types from 'vs/base/common/types'; -import { Dimension, Builder, $ } from 'vs/base/browser/builder'; +import { Builder, $ } from 'vs/base/browser/builder'; import { Sash, ISashEvent, IVerticalSashLayoutProvider, IHorizontalSashLayoutProvider, Orientation } from 'vs/base/browser/ui/sash/sash'; import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar'; import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; @@ -29,7 +29,7 @@ import { IStorageService, StorageScope } from 'vs/platform/storage/common/storag import { TabsTitleControl } from 'vs/workbench/browser/parts/editor/tabsTitleControl'; import { ITitleAreaControl } from 'vs/workbench/browser/parts/editor/titleControl'; import { NoTabsTitleControl } from 'vs/workbench/browser/parts/editor/noTabsTitleControl'; -import { IEditorStacksModel, IStacksModelChangeEvent, IEditorGroup, EditorOptions, TextEditorOptions, IEditorIdentifier, EditorInput, PREFERENCES_EDITOR_ID, TEXT_DIFF_EDITOR_ID } from 'vs/workbench/common/editor'; +import { IEditorStacksModel, IStacksModelChangeEvent, IEditorGroup, EditorOptions, TextEditorOptions, IEditorIdentifier, EditorInput } from 'vs/workbench/common/editor'; import { getCodeEditor } from 'vs/editor/browser/services/codeEditorService'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { editorBackground, contrastBorder, activeContrastBorder } from 'vs/platform/theme/common/colorRegistry'; @@ -40,6 +40,7 @@ import { ResourcesDropHandler, LocalSelectionTransfer, DraggedEditorIdentifier } import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IPartService } from 'vs/workbench/services/part/common/partService'; + export enum Rochade { NONE, TWO_TO_ONE, @@ -74,7 +75,7 @@ export interface IEditorGroupsControl { updateProgress(position: Position, state: ProgressState): void; updateTitleAreas(refreshActive?: boolean): void; - layout(dimension: Dimension): void; + layout(dimension: DOM.Dimension): void; layout(position: Position): void; arrangeGroups(arrangement: GroupArrangement): void; @@ -118,8 +119,8 @@ export class EditorGroupsControl extends Themable implements IEditorGroupsContro private stacks: IEditorStacksModel; - private parent: Builder; - private dimension: Dimension; + private parent: HTMLElement; + private dimension: DOM.Dimension; private dragging: boolean; private layoutVertically: boolean; @@ -168,7 +169,7 @@ export class EditorGroupsControl extends Themable implements IEditorGroupsContro private transfer = LocalSelectionTransfer.getInstance(); constructor( - parent: Builder, + parent: HTMLElement, groupOrientation: GroupOrientation, @IWorkbenchEditorService private editorService: IWorkbenchEditorService, @IEditorGroupService private editorGroupService: IEditorGroupService, @@ -185,7 +186,7 @@ export class EditorGroupsControl extends Themable implements IEditorGroupsContro this.stacks = editorGroupService.getStacksModel(); this.parent = parent; - this.dimension = new Dimension(0, 0); + this.dimension = new DOM.Dimension(0, 0); this.silos = []; this.silosSize = []; @@ -288,6 +289,7 @@ export class EditorGroupsControl extends Themable implements IEditorGroupsContro titleControl.dispose(); titleContainer.empty(); this.createTitleControl(this.stacks.groupAt(position), this.silos[position], titleContainer, this.getInstantiationService(position)); + this.layoutTitleControl(position); } // Refresh title when layout options change @@ -366,8 +368,8 @@ export class EditorGroupsControl extends Themable implements IEditorGroupsContro this.trackFocus(editor, position); // Find target container and build into - const target = this.silos[position].child(); - editor.getContainer().build(target); + const target = this.silos[position].child().getHTMLElement(); + target.appendChild(editor.getContainer()); // Adjust layout according to provided ratios (used when restoring multiple editors at once) if (ratio && (ratio.length === 2 || ratio.length === 3)) { @@ -439,7 +441,7 @@ export class EditorGroupsControl extends Themable implements IEditorGroupsContro } // Show editor container - editor.getContainer().show(); + DOM.show(editor.getContainer()); } private getVisibleEditorCount(): number { @@ -551,7 +553,11 @@ export class EditorGroupsControl extends Themable implements IEditorGroupsContro this.clearPosition(position); // Take editor container offdom and hide - editor.getContainer().offDOM().hide(); + const editorContainer = editor.getContainer(); + if (editorContainer.parentNode) { + editorContainer.parentNode.removeChild(editorContainer); + } + DOM.hide(editorContainer); // Adjust layout and rochade if instructed to do so if (layoutAndRochade) { @@ -777,10 +783,10 @@ export class EditorGroupsControl extends Themable implements IEditorGroupsContro this.layoutVertically = (orientation !== 'horizontal'); // Editor Layout - const verticalLayouting = this.parent.hasClass('vertical-layout'); + const verticalLayouting = DOM.hasClass(this.parent, 'vertical-layout'); if (verticalLayouting !== this.layoutVertically) { - this.parent.removeClass('vertical-layout', 'horizontal-layout'); - this.parent.addClass(this.layoutVertically ? 'vertical-layout' : 'horizontal-layout'); + DOM.removeClasses(this.parent, 'vertical-layout', 'horizontal-layout'); + DOM.addClass(this.parent, this.layoutVertically ? 'vertical-layout' : 'horizontal-layout'); this.sashOne.setOrientation(this.layoutVertically ? Orientation.VERTICAL : Orientation.HORIZONTAL); this.sashTwo.setOrientation(this.layoutVertically ? Orientation.VERTICAL : Orientation.HORIZONTAL); @@ -965,16 +971,16 @@ export class EditorGroupsControl extends Themable implements IEditorGroupsContro private create(): void { // Store layout as class property - this.parent.addClass(this.layoutVertically ? 'vertical-layout' : 'horizontal-layout'); + DOM.addClass(this.parent, this.layoutVertically ? 'vertical-layout' : 'horizontal-layout'); // Allow to drop into container to open - this.enableDropTarget(this.parent.getHTMLElement()); + this.enableDropTarget(this.parent); // Silo One this.silos[Position.ONE] = $(this.parent).div({ class: 'one-editor-silo editor-one' }); // Sash One - this.sashOne = new Sash(this.parent.getHTMLElement(), this, { baseSize: 5, orientation: this.layoutVertically ? Orientation.VERTICAL : Orientation.HORIZONTAL }); + this.sashOne = new Sash(this.parent, this, { baseSize: 5, orientation: this.layoutVertically ? Orientation.VERTICAL : Orientation.HORIZONTAL }); this.toUnbind.push(this.sashOne.onDidStart(() => this.onSashOneDragStart())); this.toUnbind.push(this.sashOne.onDidChange((e: ISashEvent) => this.onSashOneDrag(e))); this.toUnbind.push(this.sashOne.onDidEnd(() => this.onSashOneDragEnd())); @@ -985,7 +991,7 @@ export class EditorGroupsControl extends Themable implements IEditorGroupsContro this.silos[Position.TWO] = $(this.parent).div({ class: 'one-editor-silo editor-two' }); // Sash Two - this.sashTwo = new Sash(this.parent.getHTMLElement(), this, { baseSize: 5, orientation: this.layoutVertically ? Orientation.VERTICAL : Orientation.HORIZONTAL }); + this.sashTwo = new Sash(this.parent, this, { baseSize: 5, orientation: this.layoutVertically ? Orientation.VERTICAL : Orientation.HORIZONTAL }); this.toUnbind.push(this.sashTwo.onDidStart(() => this.onSashTwoDragStart())); this.toUnbind.push(this.sashTwo.onDidChange((e: ISashEvent) => this.onSashTwoDrag(e))); this.toUnbind.push(this.sashTwo.onDidEnd(() => this.onSashTwoDragEnd())); @@ -1030,9 +1036,9 @@ export class EditorGroupsControl extends Themable implements IEditorGroupsContro this.createTitleControl(this.stacks.groupAt(position), silo, titleContainer, instantiationService); // Progress Bar - const progressBar = new ProgressBar($(container)); + const progressBar = new ProgressBar(container.getHTMLElement()); this.toUnbind.push(attachProgressBarStyler(progressBar, this.themeService)); - progressBar.getContainer().hide(); + progressBar.hide(); container.setProperty(EditorGroupsControl.PROGRESS_BAR_CONTROL_KEY, progressBar); // associate with container // Sash for first position to support centered editor layout @@ -1281,7 +1287,7 @@ export class EditorGroupsControl extends Themable implements IEditorGroupsContro if (!overlay) { const containers = $this.visibleEditors.filter(e => !!e).map(e => e.getContainer()); containers.forEach((container, index) => { - if (container && DOM.isAncestor(target, container.getHTMLElement())) { + if (container && DOM.isAncestor(target, container)) { const activeContrastBorderColor = $this.getColor(activeContrastBorder); overlay = $('div').style({ top: $this.tabOptions.showTabs ? `${EditorGroupsControl.EDITOR_TITLE_HEIGHT}px` : 0, @@ -1624,11 +1630,11 @@ export class EditorGroupsControl extends Themable implements IEditorGroupsContro let borderColor = null; if (isDragging) { - this.parent.addClass('dragging'); + DOM.addClass(this.parent, 'dragging'); silo.addClass('dragging'); borderColor = this.getColor(EDITOR_GROUP_BORDER) || this.getColor(contrastBorder); } else { - this.parent.removeClass('dragging'); + DOM.removeClass(this.parent, 'dragging'); silo.removeClass('dragging'); } @@ -2010,17 +2016,17 @@ export class EditorGroupsControl extends Themable implements IEditorGroupsContro return this.dragging; } - public layout(dimension: Dimension): void; + public layout(dimension: DOM.Dimension): void; public layout(position: Position): void; public layout(arg: any): void { - if (arg instanceof Dimension) { - this.layoutControl(arg); + if (arg instanceof DOM.Dimension) { + this.layoutControl(arg); } else { this.layoutEditor(arg); } } - private layoutControl(dimension: Dimension): void { + private layoutControl(dimension: DOM.Dimension): void { let oldDimension = this.dimension; this.dimension = dimension; @@ -2138,8 +2144,8 @@ export class EditorGroupsControl extends Themable implements IEditorGroupsContro }); // Layout centered Editor (only in vertical layout when one group is opened) - const id = this.visibleEditors[Position.ONE] ? this.visibleEditors[Position.ONE].getId() : undefined; - const doCentering = this.partService.isEditorLayoutCentered() && this.stacks.groups.length === 1 && id !== PREFERENCES_EDITOR_ID && id !== TEXT_DIFF_EDITOR_ID; + const doCentering = this.partService.isEditorLayoutCentered() && this.stacks.groups.length === 1 && + this.visibleEditors[Position.ONE] && this.visibleEditors[Position.ONE].supportsCenteredLayout(); if (doCentering && !this.centeredEditorActive) { this.centeredEditorSashLeft.show(); this.centeredEditorSashRight.show(); @@ -2166,16 +2172,18 @@ export class EditorGroupsControl extends Themable implements IEditorGroupsContro }); // Layout title controls - POSITIONS.forEach(position => { - const siloWidth = this.layoutVertically ? this.silosSize[position] : this.dimension.width; - - this.getTitleAreaControl(position).layout(new Dimension(siloWidth, EditorGroupsControl.EDITOR_TITLE_HEIGHT)); - }); + POSITIONS.forEach(position => this.layoutTitleControl(position)); // Update minimized state this.updateMinimizedState(); } + private layoutTitleControl(position: Position): void { + const siloWidth = this.layoutVertically ? this.silosSize[position] : this.dimension.width; + + this.getTitleAreaControl(position).layout(new DOM.Dimension(siloWidth, EditorGroupsControl.EDITOR_TITLE_HEIGHT)); + } + private layoutEditor(position: Position): void { const editorSize = this.silosSize[position]; const editor = this.visibleEditors[position]; @@ -2198,10 +2206,10 @@ export class EditorGroupsControl extends Themable implements IEditorGroupsContro } const editorContainer = editor.getContainer(); - editorContainer.style('margin-left', this.centeredEditorActive ? `${editorPosition}px` : null); - editorContainer.style('width', this.centeredEditorActive ? `${editorWidth}px` : null); - editorContainer.style('border-color', this.centeredEditorActive ? this.getColor(EDITOR_GROUP_BORDER) || this.getColor(contrastBorder) : null); - editor.layout(new Dimension(editorWidth, editorHeight)); + editorContainer.style.marginLeft = this.centeredEditorActive ? `${editorPosition}px` : null; + editorContainer.style.width = this.centeredEditorActive ? `${editorWidth}px` : null; + editorContainer.style.borderColor = this.centeredEditorActive ? this.getColor(EDITOR_GROUP_BORDER) || this.getColor(contrastBorder) : null; + editor.layout(new DOM.Dimension(editorWidth, editorHeight)); } } @@ -2266,13 +2274,13 @@ export class EditorGroupsControl extends Themable implements IEditorGroupsContro switch (state) { case ProgressState.INFINITE: - progressbar.infinite().getContainer().show(); + progressbar.infinite().show(); break; case ProgressState.DONE: - progressbar.done().getContainer().hide(); + progressbar.done().hide(); break; case ProgressState.STOP: - progressbar.stop().getContainer().hide(); + progressbar.stop().hide(); break; } } diff --git a/src/vs/workbench/browser/parts/editor/editorPart.ts b/src/vs/workbench/browser/parts/editor/editorPart.ts index 86c952e038e..6a1c4551fe4 100644 --- a/src/vs/workbench/browser/parts/editor/editorPart.ts +++ b/src/vs/workbench/browser/parts/editor/editorPart.ts @@ -9,7 +9,6 @@ import 'vs/css!./media/editorpart'; import 'vs/workbench/browser/parts/editor/editor.contribution'; import { TPromise } from 'vs/base/common/winjs.base'; import { Registry } from 'vs/platform/registry/common/platform'; -import { Dimension, Builder, $ } from 'vs/base/browser/builder'; import * as nls from 'vs/nls'; import * as strings from 'vs/base/common/strings'; import * as arrays from 'vs/base/common/arrays'; @@ -40,7 +39,7 @@ 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 { createCSSRule, Dimension, addClass, removeClass } from 'vs/base/browser/dom'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { join } from 'vs/base/common/paths'; import { IEditorDescriptor, IEditorRegistry, Extensions as EditorExtensions } from 'vs/workbench/browser/editor'; @@ -103,15 +102,16 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupService private forceHideTabs: boolean; private doNotFireTabOptionsChanged: boolean; private revealIfOpen: boolean; + private ignoreOpenEditorErrors: boolean; + private textCompareEditorVisible: IContextKey; - private _onEditorsChanged: ThrottledEmitter; + private readonly _onEditorsChanged: ThrottledEmitter; private readonly _onEditorOpening: Emitter; private readonly _onEditorGroupMoved: Emitter; private readonly _onEditorOpenFail: Emitter; private readonly _onGroupOrientationChanged: Emitter; private readonly _onTabOptionsChanged: Emitter; - - private textCompareEditorVisible: IContextKey; + private readonly _onLayout: Emitter; // The following data structures are partitioned into array of Position as provided by Services.POSITION array private visibleEditors: BaseEditor[]; @@ -120,9 +120,6 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupService private pendingEditorInputsToClose: EditorIdentifier[]; private pendingEditorInputCloseTimeout: number; - private onLayoutEmitter = new Emitter(); - public onLayout = this.onLayoutEmitter.event; - constructor( id: string, restoreFromStorage: boolean, @@ -144,6 +141,7 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupService this._onEditorOpenFail = new Emitter(); this._onGroupOrientationChanged = new Emitter(); this._onTabOptionsChanged = new Emitter(); + this._onLayout = new Emitter(); this.visibleEditors = []; @@ -293,6 +291,10 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupService this.editorGroupsControl.resizeGroup(position, groupSizeChange); } + public get onLayout(): Event { + return this._onLayout.event; + } + public get onEditorsChanged(): Event { return this._onEditorsChanged.event; } @@ -470,11 +472,12 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupService // Create editor as needed if (!editor.getContainer()) { - editor.create($().div({ - 'class': 'editor-container', - 'role': 'tabpanel', - id: descriptor.getId() - })); + const editorContainer = document.createElement('div'); + editorContainer.id = descriptor.getId(); + addClass(editorContainer, 'editor-container'); + editorContainer.setAttribute('role', 'tabpanel'); + + editor.create(editorContainer); } return editor; @@ -546,8 +549,9 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupService // Stop loading promise if any monitor.cancel(); - // Report error only if this was not us restoring previous error state - if (this.partService.isCreated() && !errors.isPromiseCanceledError(error)) { + // Report error only if this was not us restoring previous error state or + // we are told to ignore errors that occur from opening an editor + if (this.partService.isCreated() && !errors.isPromiseCanceledError(error) && !this.ignoreOpenEditorErrors) { const actions: INotificationActions = { primary: [] }; if (errors.isErrorWithActions(error)) { actions.primary = (error as errors.IErrorWithActions).actions; @@ -559,7 +563,7 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupService actions }); - once(handle.onDidDispose)(() => dispose(actions.primary)); + once(handle.onDidClose)(() => dispose(actions.primary)); } this.editorGroupsControl.updateProgress(position, ProgressState.DONE); @@ -569,7 +573,7 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupService // Recover by closing the active editor (if the input is still the active one) if (group.activeEditor === input) { - this.doCloseActiveEditor(group, !(options && options.preserveFocus) /* still preserve focus as needed */); + this.doCloseActiveEditor(group, !(options && options.preserveFocus) /* still preserve focus as needed */, true /* from error */); } } @@ -603,7 +607,7 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupService } } - private doCloseActiveEditor(group: EditorGroup, focusNext = true): void { + private doCloseActiveEditor(group: EditorGroup, focusNext = true, fromError?: boolean): void { const position = this.stacks.positionOfGroup(group); // Update stacks model @@ -616,7 +620,22 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupService // Otherwise open next active else { - this.openEditor(group.activeEditor, !focusNext ? EditorOptions.create({ preserveFocus: true }) : null, position).done(null, errors.onUnexpectedError); + // When closing an editor due to an error we can end up in a loop where we continue closing + // editors that fail to open (e.g. when the file no longer exists). We do not want to show + // repeated errors in this case to the user. As such, if we open the next editor and we are + // in a scope of a previous editor failing, we silence the input errors until the editor is + // opened. + if (fromError) { + this.ignoreOpenEditorErrors = true; + } + + this.openEditor(group.activeEditor, !focusNext ? EditorOptions.create({ preserveFocus: true }) : null, position).done(() => { + this.ignoreOpenEditorErrors = false; + }, error => { + errors.onUnexpectedError(error); + + this.ignoreOpenEditorErrors = false; + }); } } @@ -826,16 +845,18 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupService // Close: By Filter or all else { - editorsToClose = group.getEditors(true /* in MRU order */); filter = filterOrEditors || Object.create(null); + const hasDirection = !types.isUndefinedOrNull(filter.direction); + editorsToClose = group.getEditors(!hasDirection /* in MRU order only if direction is not specified */); + // Filter: saved only if (filter.savedOnly) { editorsToClose = editorsToClose.filter(e => !e.isDirty()); } // Filter: direction (left / right) - else if (!types.isUndefinedOrNull(filter.direction)) { + else if (hasDirection) { editorsToClose = (filter.direction === Direction.LEFT) ? editorsToClose.slice(0, group.indexOf(filter.except)) : editorsToClose.slice(group.indexOf(filter.except) + 1); } @@ -1134,12 +1155,12 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupService return this.editorGroupsControl.getGroupOrientation(); } - public createContentArea(parent: Builder): Builder { + public createContentArea(parent: HTMLElement): HTMLElement { // Content Container - const contentArea = $(parent) - .div() - .addClass('content'); + const contentArea = document.createElement('div'); + addClass(contentArea, 'content'); + parent.appendChild(contentArea); // get settings this.memento = this.getMemento(this.storageService, MementoScope.WORKSPACE); @@ -1157,19 +1178,19 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupService // Part container const container = this.getContainer(); - container.style('background-color', this.getColor(editorBackground)); + container.style.backgroundColor = this.getColor(editorBackground); // Content area const content = this.getContentArea(); const groupCount = this.stacks.groups.length; if (groupCount > 1) { - content.addClass('multiple-groups'); + addClass(content, 'multiple-groups'); } else { - content.removeClass('multiple-groups'); + removeClass(content, 'multiple-groups'); } - content.style('background-color', groupCount > 0 ? this.getColor(EDITOR_GROUP_BACKGROUND) : null); + content.style.backgroundColor = groupCount > 0 ? this.getColor(EDITOR_GROUP_BACKGROUND) : null; } private onGroupFocusChanged(): void { @@ -1467,7 +1488,7 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupService this.dimension = sizes[1]; this.editorGroupsControl.layout(this.dimension); - this.onLayoutEmitter.fire(dimension); + this._onLayout.fire(dimension); return sizes; } @@ -1500,6 +1521,9 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupService this._onEditorOpening.dispose(); this._onEditorGroupMoved.dispose(); this._onEditorOpenFail.dispose(); + this._onGroupOrientationChanged.dispose(); + this._onTabOptionsChanged.dispose(); + this._onLayout.dispose(); // Reset Tokens this.editorOpenToken = []; diff --git a/src/vs/workbench/browser/parts/editor/editorStatus.ts b/src/vs/workbench/browser/parts/editor/editorStatus.ts index 01ed2d2da59..6476c972704 100644 --- a/src/vs/workbench/browser/parts/editor/editorStatus.ts +++ b/src/vs/workbench/browser/parts/editor/editorStatus.ts @@ -59,25 +59,46 @@ import { Button } from 'vs/base/browser/ui/button/button'; import { Schemas } from 'vs/base/common/network'; import { IAnchor } from 'vs/base/browser/ui/contextview/contextview'; import { Themable } from 'vs/workbench/common/theme'; +import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; -// TODO@Sandeep layer breaker -// tslint:disable-next-line:import-patterns -import { IPreferencesService } from 'vs/workbench/parts/preferences/common/preferences'; +class SideBySideEditorEncodingSupport implements IEncodingSupport { + constructor(private master: IEncodingSupport, private details: IEncodingSupport) { } -function toEditorWithEncodingSupport(input: IEditorInput): IEncodingSupport { - if (input instanceof SideBySideEditorInput) { - input = input.master; + public getEncoding(): string { + return this.master.getEncoding(); // always report from modified (right hand) side } + public setEncoding(encoding: string, mode: EncodingMode): void { + [this.master, this.details].forEach(s => s.setEncoding(encoding, mode)); + } +} + +function toEditorWithEncodingSupport(input: IEditorInput): IEncodingSupport { + + // Untitled Editor if (input instanceof UntitledEditorInput) { return input; } + // Side by Side (diff) Editor + if (input instanceof SideBySideEditorInput) { + const masterEncodingSupport = toEditorWithEncodingSupport(input.master); + const detailsEncodingSupport = toEditorWithEncodingSupport(input.details); + + if (masterEncodingSupport && detailsEncodingSupport) { + return new SideBySideEditorEncodingSupport(masterEncodingSupport, detailsEncodingSupport); + } + + return masterEncodingSupport; + } + + // File or Resource Editor let encodingSupport = input as IFileEditorInput; if (types.areFunctions(encodingSupport.setEncoding, encodingSupport.getEncoding)) { return encodingSupport; } + // Unsupported for any other editor return null; } @@ -311,7 +332,7 @@ export class EditorStatus implements IStatusbarItem { hide(this.selectionElement); this.indentationElement = append(this.element, $('a.editor-status-indentation')); - this.indentationElement.title = nls.localize('indentation', "Indentation"); + this.indentationElement.title = nls.localize('selectIndentation', "Select Indentation"); this.indentationElement.onclick = () => this.onIndentationClick(); hide(this.indentationElement); diff --git a/src/vs/workbench/browser/parts/editor/media/resourceviewer.css b/src/vs/workbench/browser/parts/editor/media/resourceviewer.css index 6f4e21c6ef5..45eef21f8c3 100644 --- a/src/vs/workbench/browser/parts/editor/media/resourceviewer.css +++ b/src/vs/workbench/browser/parts/editor/media/resourceviewer.css @@ -54,8 +54,9 @@ cursor: zoom-out; } -.monaco-resource-viewer .open-external, -.monaco-resource-viewer .open-external:hover { +.monaco-resource-viewer .embedded-link, +.monaco-resource-viewer .embedded-link:hover { cursor: pointer; text-decoration: underline; + margin-left: 5px; } diff --git a/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts b/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts index 6b0144487c7..948465e40ae 100644 --- a/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts @@ -88,7 +88,7 @@ export class NoTabsTitleControl extends TitleControl { // - click on toolbar: should trigger actions within // - mouse click: do not focus group if there are more than one as it otherwise makes group DND funky // - touch: always focus - else if ((this.stacks.groups.length === 1 || !(e instanceof MouseEvent)) && !DOM.isAncestor(((e as GestureEvent).initialTarget || e.target || e.srcElement) as HTMLElement, this.editorActionsToolbar.getContainer().getHTMLElement())) { + else if ((this.stacks.groups.length === 1 || !(e instanceof MouseEvent)) && !DOM.isAncestor(((e as GestureEvent).initialTarget || e.target || e.srcElement) as HTMLElement, this.editorActionsToolbar.getContainer())) { this.editorGroupService.focusGroup(group); } } diff --git a/src/vs/workbench/browser/parts/editor/resourceViewer.ts b/src/vs/workbench/browser/parts/editor/resourceViewer.ts index 1d2ac46a693..f89c87eea31 100644 --- a/src/vs/workbench/browser/parts/editor/resourceViewer.ts +++ b/src/vs/workbench/browser/parts/editor/resourceViewer.ts @@ -10,7 +10,7 @@ import * as nls from 'vs/nls'; import * as mimes from 'vs/base/common/mime'; import URI from 'vs/base/common/uri'; import * as paths from 'vs/base/common/paths'; -import { Builder, $, Dimension } from 'vs/base/browser/builder'; +import { Builder, $ } from 'vs/base/browser/builder'; import * as DOM from 'vs/base/browser/dom'; import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; import { LRUCache } from 'vs/base/common/map'; @@ -19,7 +19,6 @@ import { clamp } from 'vs/base/common/numbers'; import { Themable } from 'vs/workbench/common/theme'; import { IStatusbarItem, StatusbarItemDescriptor, IStatusbarRegistry, Extensions, StatusbarAlignment } from 'vs/workbench/browser/parts/statusbar/statusbar'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { } from 'vs/platform/workspace/common/workspace'; import { IDisposable } from 'vs/base/common/lifecycle'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -119,7 +118,7 @@ class BinarySize { } export interface ResourceViewerContext { - layout(dimension: Dimension): void; + layout(dimension: DOM.Dimension): void; } /** @@ -127,26 +126,42 @@ export interface ResourceViewerContext { * progress of the binary resource. */ export class ResourceViewer { + + private static readonly MAX_OPEN_INTERNAL_SIZE = BinarySize.MB * 200; // max size until we offer an action to open internally + public static show( descriptor: IResourceDescriptor, - container: Builder, + container: HTMLElement, scrollbar: DomScrollableElement, - openExternal: (uri: URI) => void, + openInternalClb: (uri: URI) => void, + openExternalClb: (uri: URI) => void, metadataClb: (meta: string) => void - ): ResourceViewerContext { + ): ResourceViewerContext | null { + // Ensure CSS class $(container).setClass('monaco-resource-viewer'); + // Images if (ResourceViewer.isImageResource(descriptor)) { - return ImageView.create(container, descriptor, scrollbar, openExternal, metadataClb); + return ImageView.create(container, descriptor, scrollbar, openExternalClb, metadataClb); + } + + // Large Files + if (descriptor.size > ResourceViewer.MAX_OPEN_INTERNAL_SIZE) { + FileTooLargeFileView.create(container, descriptor, scrollbar, metadataClb); + } + + // Seemingly Binary Files + else { + FileSeemsBinaryFileView.create(container, descriptor, scrollbar, openInternalClb, metadataClb); } - GenericBinaryFileView.create(container, metadataClb, descriptor, scrollbar); return null; } private static isImageResource(descriptor: IResourceDescriptor) { const mime = ResourceViewer.getMime(descriptor); + return mime.indexOf('image/') >= 0; } @@ -158,6 +173,7 @@ export class ResourceViewer { mime = mapExtToMediaMimes[ext.toLowerCase()]; } } + return mime || mimes.MIME_BINARY; } } @@ -167,17 +183,18 @@ class ImageView { private static readonly BASE64_MARKER = 'base64,'; public static create( - container: Builder, + container: HTMLElement, descriptor: IResourceDescriptor, scrollbar: DomScrollableElement, - openExternal: (uri: URI) => void, + openExternalClb: (uri: URI) => void, metadataClb: (meta: string) => void ): ResourceViewerContext | null { if (ImageView.shouldShowImageInline(descriptor)) { return InlineImageView.create(container, descriptor, scrollbar, metadataClb); } - LargeImageView.create(container, descriptor, openExternal); + LargeImageView.create(container, descriptor, openExternalClb); + return null; } @@ -203,43 +220,81 @@ class ImageView { class LargeImageView { public static create( - container: Builder, + container: HTMLElement, descriptor: IResourceDescriptor, - openExternal: (uri: URI) => void + openExternalClb: (uri: URI) => void ) { + const size = BinarySize.formatSize(descriptor.size); + const imageContainer = $(container) .empty() .p({ - text: nls.localize('largeImageError', "The file size of the image is too large (>1MB) to display in the editor. ") + text: nls.localize('largeImageError', "The image is not displayed in the editor because it is too large ({0}).", size) }); if (descriptor.resource.scheme !== Schemas.data) { imageContainer.append($('a', { role: 'button', - class: 'open-external', + class: 'embedded-link', text: nls.localize('resourceOpenExternalButton', "Open image using external program?") }).on(DOM.EventType.CLICK, (e) => { - openExternal(descriptor.resource); + openExternalClb(descriptor.resource); })); } } } -class GenericBinaryFileView { +class FileTooLargeFileView { public static create( - container: Builder, - metadataClb: (meta: string) => void, + container: HTMLElement, descriptor: IResourceDescriptor, - scrollbar: DomScrollableElement + scrollbar: DomScrollableElement, + metadataClb: (meta: string) => void ) { + const size = BinarySize.formatSize(descriptor.size); + $(container) .empty() .span({ - text: nls.localize('nativeBinaryError', "The file will not be displayed in the editor because it is either binary, very large or uses an unsupported text encoding.") + text: nls.localize('nativeFileTooLargeError', "The file is not displayed in the editor because it is too large ({0}).", size) }); + + if (metadataClb) { + metadataClb(size); + } + + scrollbar.scanDomNode(); + } +} + +class FileSeemsBinaryFileView { + public static create( + container: HTMLElement, + descriptor: IResourceDescriptor, + scrollbar: DomScrollableElement, + openInternalClb: (uri: URI) => void, + metadataClb: (meta: string) => void + ) { + const binaryContainer = $(container) + .empty() + .p({ + text: nls.localize('nativeBinaryError', "The file is not displayed in the editor because it is either binary or uses an unsupported text encoding.") + }); + + if (descriptor.resource.scheme !== Schemas.data) { + binaryContainer.append($('a', { + role: 'button', + class: 'embedded-link', + text: nls.localize('openAsText', "Do you want to open it anyway?") + }).on(DOM.EventType.CLICK, (e) => { + openInternalClb(descriptor.resource); + })); + } + if (metadataClb) { metadataClb(BinarySize.formatSize(descriptor.size)); } + scrollbar.scanDomNode(); } } @@ -266,7 +321,7 @@ class ZoomStatusbarItem extends Themable implements IStatusbarItem { private onEditorsChanged(): void { this.hide(); - this.onSelectScale = undefined; + this.onSelectScale = void 0; } public show(scale: Scale, onSelectScale: (scale: number) => void) { @@ -295,6 +350,7 @@ class ZoomStatusbarItem extends Themable implements IStatusbarItem { .getHTMLElement(); this.statusBarItem.style.display = 'none'; } + return this; } @@ -306,10 +362,11 @@ class ZoomStatusbarItem extends Themable implements IStatusbarItem { private get zoomActions(): Action[] { const scales: Scale[] = [10, 5, 2, 1, 0.5, 0.2, 'fit']; return scales.map(scale => - new Action('zoom.' + scale, ZoomStatusbarItem.zoomLabel(scale), undefined, undefined, () => { + new Action(`zoom.${scale}`, ZoomStatusbarItem.zoomLabel(scale), void 0, void 0, () => { if (this.onSelectScale) { this.onSelectScale(scale); } + return null; })); } @@ -376,13 +433,13 @@ class InlineImageView { private static readonly imageStateCache = new LRUCache(100); public static create( - container: Builder, + container: HTMLElement, descriptor: IResourceDescriptor, scrollbar: DomScrollableElement, metadataClb: (meta: string) => void ) { const context = { - layout(dimension: Dimension) { } + layout(dimension: DOM.Dimension) { } }; const cacheKey = descriptor.resource.toString(); @@ -585,6 +642,4 @@ class InlineImageView { return cached.src; } -} - - +} \ No newline at end of file diff --git a/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts b/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts index fd414145054..5e3e963f77a 100644 --- a/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts +++ b/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts @@ -5,14 +5,11 @@ import { TPromise } from 'vs/base/common/winjs.base'; import * as DOM from 'vs/base/browser/dom'; -import { Dimension, Builder } from 'vs/base/browser/builder'; - import { Registry } from 'vs/platform/registry/common/platform'; import { EditorInput, EditorOptions, SideBySideEditorInput } from 'vs/workbench/common/editor'; import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; import { IEditorControl, Position, IEditor } from 'vs/platform/editor/common/editor'; import { VSash } from 'vs/base/browser/ui/sash/sash'; - import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IThemeService } from 'vs/platform/theme/common/themeService'; @@ -23,7 +20,7 @@ export class SideBySideEditor extends BaseEditor { public static readonly ID: string = 'workbench.editor.sidebysideEditor'; - private dimension: Dimension; + private dimension: DOM.Dimension; protected masterEditor: BaseEditor; private masterEditorContainer: HTMLElement; @@ -41,10 +38,9 @@ export class SideBySideEditor extends BaseEditor { super(SideBySideEditor.ID, telemetryService, themeService); } - protected createEditor(parent: Builder): void { - const parentElement = parent.getHTMLElement(); - DOM.addClass(parentElement, 'side-by-side-editor'); - this.createSash(parentElement); + protected createEditor(parent: HTMLElement): void { + DOM.addClass(parent, 'side-by-side-editor'); + this.createSash(parent); } public setInput(newInput: SideBySideEditorInput, options?: EditorOptions): TPromise { @@ -90,7 +86,7 @@ export class SideBySideEditor extends BaseEditor { } } - public layout(dimension: Dimension): void { + public layout(dimension: DOM.Dimension): void { this.dimension = dimension; this.sash.setDimenesion(this.dimension); } @@ -110,6 +106,10 @@ export class SideBySideEditor extends BaseEditor { return this.detailsEditor; } + public supportsCenteredLayout(): boolean { + return false; + } + private updateInput(oldInput: SideBySideEditorInput, newInput: SideBySideEditorInput, options?: EditorOptions): void { if (!newInput.matches(oldInput)) { if (oldInput) { @@ -137,7 +137,7 @@ export class SideBySideEditor extends BaseEditor { const descriptor = Registry.as(EditorExtensions.Editors).getEditor(editorInput); const editor = descriptor.instantiate(this.instantiationService); - editor.create(new Builder(container)); + editor.create(container); editor.setVisible(this.isVisible(), this.position); return editor; @@ -151,7 +151,7 @@ export class SideBySideEditor extends BaseEditor { } private createEditorContainers(): void { - const parentElement = this.getContainer().getHTMLElement(); + const parentElement = this.getContainer(); this.detailsEditorContainer = DOM.append(parentElement, DOM.$('.details-editor-container')); this.detailsEditorContainer.style.position = 'absolute'; this.masterEditorContainer = DOM.append(parentElement, DOM.$('.master-editor-container')); @@ -188,12 +188,12 @@ export class SideBySideEditor extends BaseEditor { this.masterEditorContainer.style.height = `${this.dimension.height}px`; this.masterEditorContainer.style.left = `${splitPoint}px`; - this.detailsEditor.layout(new Dimension(detailsEditorWidth, this.dimension.height)); - this.masterEditor.layout(new Dimension(masterEditorWidth, this.dimension.height)); + this.detailsEditor.layout(new DOM.Dimension(detailsEditorWidth, this.dimension.height)); + this.masterEditor.layout(new DOM.Dimension(masterEditorWidth, this.dimension.height)); } private disposeEditors(): void { - const parentContainer = this.getContainer().getHTMLElement(); + const parentContainer = this.getContainer(); if (this.detailsEditor) { this.detailsEditor.dispose(); this.detailsEditor = null; @@ -216,4 +216,4 @@ export class SideBySideEditor extends BaseEditor { this.disposeEditors(); super.dispose(); } -} \ 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 ece29d8aec6..a010f2c32b2 100644 --- a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts @@ -38,7 +38,6 @@ import { ServiceCollection } from 'vs/platform/instantiation/common/serviceColle import { IThemeService, registerThemingParticipant, ITheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; import { TAB_INACTIVE_BACKGROUND, TAB_ACTIVE_BACKGROUND, TAB_ACTIVE_FOREGROUND, TAB_INACTIVE_FOREGROUND, TAB_BORDER, EDITOR_DRAG_AND_DROP_BACKGROUND, TAB_UNFOCUSED_ACTIVE_FOREGROUND, TAB_UNFOCUSED_INACTIVE_FOREGROUND, TAB_UNFOCUSED_ACTIVE_BORDER, TAB_ACTIVE_BORDER, TAB_HOVER_BACKGROUND, TAB_HOVER_BORDER, TAB_UNFOCUSED_HOVER_BACKGROUND, TAB_UNFOCUSED_HOVER_BORDER, EDITOR_GROUP_HEADER_TABS_BACKGROUND, EDITOR_GROUP_BACKGROUND, WORKBENCH_BACKGROUND } from 'vs/workbench/common/theme'; import { activeContrastBorder, contrastBorder, editorBackground } from 'vs/platform/theme/common/colorRegistry'; -import { Dimension } from 'vs/base/browser/builder'; import { ResourcesDropHandler, fillResourceDataTransfers, LocalSelectionTransfer, DraggedEditorIdentifier } from 'vs/workbench/browser/dnd'; import { Color } from 'vs/base/common/color'; import { INotificationService } from 'vs/platform/notification/common/notification'; @@ -60,7 +59,7 @@ export class TabsTitleControl extends TitleControl { private scrollbar: ScrollableElement; private tabDisposeables: IDisposable[]; private blockRevealActiveTab: boolean; - private dimension: Dimension; + private dimension: DOM.Dimension; private layoutScheduled: IDisposable; private transfer = LocalSelectionTransfer.getInstance(); @@ -555,7 +554,7 @@ export class TabsTitleControl extends TitleControl { return tabContainer; } - public layout(dimension: Dimension): void { + public layout(dimension: DOM.Dimension): void { if (!this.activeTab || !dimension) { return; } @@ -573,7 +572,7 @@ export class TabsTitleControl extends TitleControl { } } - private doLayout(dimension: Dimension): void { + private doLayout(dimension: DOM.Dimension): void { const visibleContainerWidth = this.tabsContainer.offsetWidth; const totalContainerWidth = this.tabsContainer.scrollWidth; @@ -926,7 +925,7 @@ registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => { if (tabHoverBackground) { collector.addRule(` .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title.active .tabs-container > .tab:hover { - background: ${tabHoverBackground} !important; + background-color: ${tabHoverBackground} !important; } `); } @@ -935,7 +934,7 @@ registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => { if (tabUnfocusedHoverBackground) { collector.addRule(` .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title.inactive .tabs-container > .tab:hover { - background: ${tabUnfocusedHoverBackground} !important; + background-color: ${tabUnfocusedHoverBackground} !important; } `); } diff --git a/src/vs/workbench/browser/parts/editor/textDiffEditor.ts b/src/vs/workbench/browser/parts/editor/textDiffEditor.ts index d82743f7c8e..50a3dfd9e8d 100644 --- a/src/vs/workbench/browser/parts/editor/textDiffEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textDiffEditor.ts @@ -9,7 +9,6 @@ import 'vs/css!./media/textdiffeditor'; import { TPromise } from 'vs/base/common/winjs.base'; import * as nls from 'vs/nls'; import * as objects from 'vs/base/common/objects'; -import { Builder } from 'vs/base/browser/builder'; import { Action, IAction } from 'vs/base/common/actions'; import { onUnexpectedError } from 'vs/base/common/errors'; import * as types from 'vs/base/common/types'; @@ -32,10 +31,13 @@ import { IWorkbenchEditorService, DelegatingWorkbenchEditorService } from 'vs/wo import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; -import { ScrollType } from 'vs/editor/common/editorCommon'; +import { ScrollType, IDiffEditorViewState, IDiffEditorModel } from 'vs/editor/common/editorCommon'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { Registry } from 'vs/platform/registry/common/platform'; +import URI from 'vs/base/common/uri'; +import { getCodeOrDiffEditor } from 'vs/editor/browser/services/codeEditorService'; +import { once } from 'vs/base/common/event'; /** * The text editor that leverages the diff text editor for the editing experience. @@ -45,10 +47,10 @@ export class TextDiffEditor extends BaseTextEditor { public static readonly ID = TEXT_DIFF_EDITOR_ID; private diffNavigator: DiffNavigator; + private diffNavigatorDisposables: IDisposable[]; private nextDiffAction: NavigateAction; private previousDiffAction: NavigateAction; private toggleIgnoreTrimWhitespaceAction: ToggleIgnoreTrimWhitespaceAction; - private _configurationListener: IDisposable; constructor( @ITelemetryService telemetryService: ITelemetryService, @@ -63,11 +65,12 @@ export class TextDiffEditor extends BaseTextEditor { ) { super(TextDiffEditor.ID, telemetryService, instantiationService, storageService, configurationService, themeService, textFileService, editorGroupService); - this._configurationListener = this._actualConfigurationService.onDidChangeConfiguration((e) => { + this.diffNavigatorDisposables = []; + this.toUnbind.push(this._actualConfigurationService.onDidChangeConfiguration((e) => { if (e.affectsConfiguration('diffEditor.ignoreTrimWhitespace')) { this.updateIgnoreTrimWhitespaceAction(); } - }); + })); } public getTitle(): string { @@ -78,7 +81,7 @@ export class TextDiffEditor extends BaseTextEditor { return nls.localize('textDiffEditor', "Text Diff Editor"); } - public createEditorControl(parent: Builder, configuration: IEditorOptions): IDiffEditor { + public createEditorControl(parent: HTMLElement, configuration: IEditorOptions): IDiffEditor { // Actions this.nextDiffAction = new NavigateAction(this, true); @@ -118,7 +121,7 @@ export class TextDiffEditor extends BaseTextEditor { // Create a special child of instantiator that will delegate all calls to openEditor() to the same diff editor if the input matches with the modified one const diffEditorInstantiator = this.instantiationService.createChild(new ServiceCollection([IWorkbenchEditorService, delegatingEditorService])); - return diffEditorInstantiator.createInstance(DiffEditorWidget, parent.getHTMLElement(), configuration); + return diffEditorInstantiator.createInstance(DiffEditorWidget, parent, configuration); } public setInput(input: EditorInput, options?: EditorOptions): TPromise { @@ -137,9 +140,10 @@ export class TextDiffEditor extends BaseTextEditor { } // Dispose previous diff navigator - if (this.diffNavigator) { - this.diffNavigator.dispose(); - } + this.diffNavigatorDisposables = dispose(this.diffNavigatorDisposables); + + // Remember view settings if input changes + this.saveTextDiffEditorViewState(this.input); // Set input and resolve return super.setInput(input, options).then(() => { @@ -155,27 +159,32 @@ export class TextDiffEditor extends BaseTextEditor { return null; } - // Editor + // Set Editor Model const diffEditor = this.getControl(); diffEditor.setModel((resolvedModel).textDiffEditorModel); - // Handle TextOptions - let alwaysRevealFirst = true; + // Apply Options from TextOptions + let optionsGotApplied = false; if (options && types.isFunction((options).apply)) { - const hadOptions = (options).apply(diffEditor, ScrollType.Immediate); - if (hadOptions) { - alwaysRevealFirst = false; // Do not reveal if we are instructed to open specific line/col - } + optionsGotApplied = (options).apply(diffEditor, ScrollType.Immediate); + } + + // Otherwise restore View State + let hasPreviousViewState = false; + if (!optionsGotApplied) { + hasPreviousViewState = this.restoreTextDiffEditorViewState(input); } - // Listen on diff updated changes to reveal the first change this.diffNavigator = new DiffNavigator(diffEditor, { - alwaysRevealFirst + alwaysRevealFirst: !optionsGotApplied && !hasPreviousViewState // only reveal first change if we had no options or viewstate }); - this.diffNavigator.onDidUpdate(() => { + this.diffNavigatorDisposables.push(this.diffNavigator); + + this.diffNavigatorDisposables.push(this.diffNavigator.onDidUpdate(() => { this.nextDiffAction.updateEnablement(); this.previousDiffAction.updateEnablement(); - }); + })); + this.updateIgnoreTrimWhitespaceAction(); }, error => { @@ -190,6 +199,26 @@ export class TextDiffEditor extends BaseTextEditor { }); } + public supportsCenteredLayout(): boolean { + return false; + } + + private restoreTextDiffEditorViewState(input: EditorInput): boolean { + if (input instanceof DiffEditorInput) { + const resource = this.toDiffEditorViewStateResource(input); + if (resource) { + const viewState = this.loadTextEditorViewState(resource); + if (viewState) { + this.getControl().restoreViewState(viewState); + + return true; + } + } + } + + return false; + } + private updateIgnoreTrimWhitespaceAction(): void { const ignoreTrimWhitespace = this.configurationService.getValue(this.getResource(), 'diffEditor.ignoreTrimWhitespace'); if (this.toggleIgnoreTrimWhitespaceAction) { @@ -278,9 +307,10 @@ export class TextDiffEditor extends BaseTextEditor { public clearInput(): void { // Dispose previous diff navigator - if (this.diffNavigator) { - this.diffNavigator.dispose(); - } + this.diffNavigatorDisposables = dispose(this.diffNavigatorDisposables); + + // Keep editor view state in settings to restore when coming back + this.saveTextDiffEditorViewState(this.input); // Clear Model this.getControl().setModel(null); @@ -305,14 +335,85 @@ export class TextDiffEditor extends BaseTextEditor { return super.getControl() as IDiffEditor; } - public dispose(): void { + protected loadTextEditorViewState(resource: URI): IDiffEditorViewState { + return super.loadTextEditorViewState(resource) as IDiffEditorViewState; // overridden for text diff editor support + } - // Dispose previous diff navigator - if (this.diffNavigator) { - this.diffNavigator.dispose(); + private saveTextDiffEditorViewState(input: EditorInput): void { + if (!(input instanceof DiffEditorInput)) { + return; // only supported for diff editor inputs } - this._configurationListener.dispose(); + const resource = this.toDiffEditorViewStateResource(input); + if (!resource) { + return; // unable to retrieve input resource + } + + // Clear view state if input is disposed + if (input.isDisposed()) { + super.clearTextEditorViewState([resource]); + } + + // Otherwise save it + else { + super.saveTextEditorViewState(resource); + + // Make sure to clean up when the input gets disposed + once(input.onDispose)(() => { + super.clearTextEditorViewState([resource]); + }); + } + } + + protected retrieveTextEditorViewState(resource: URI): IDiffEditorViewState { + return this.retrieveTextDiffEditorViewState(resource); // overridden for text diff editor support + } + + private retrieveTextDiffEditorViewState(resource: URI): IDiffEditorViewState { + const editor = getCodeOrDiffEditor(this).diffEditor; + if (!editor) { + return null; // not supported for non-diff editors + } + + const model = editor.getModel(); + if (!model || !model.modified || !model.original) { + return null; // view state always needs a model + } + + const modelUri = this.toDiffEditorViewStateResource(model); + if (!modelUri) { + return null; // model URI is needed to make sure we save the view state correctly + } + + if (modelUri.toString() !== resource.toString()) { + return null; // prevent saving view state for a model that is not the expected one + } + + return editor.saveViewState(); + } + + private toDiffEditorViewStateResource(modelOrInput: IDiffEditorModel | DiffEditorInput): URI { + let original: URI; + let modified: URI; + + if (modelOrInput instanceof DiffEditorInput) { + original = modelOrInput.originalInput.getResource(); + modified = modelOrInput.modifiedInput.getResource(); + } else { + original = modelOrInput.original.uri; + modified = modelOrInput.modified.uri; + } + + if (!original || !modified) { + return null; + } + + // create a URI that is the Base64 concatenation of original + modified resource + return URI.from({ scheme: 'diff', path: `${btoa(original.toString())}${btoa(modified.toString())}` }); + } + + public dispose(): void { + this.diffNavigatorDisposables = dispose(this.diffNavigatorDisposables); super.dispose(); } @@ -372,4 +473,4 @@ class ToggleIgnoreTrimWhitespaceAction extends Action { this._configurationService.updateValue(`diffEditor.ignoreTrimWhitespace`, !this._isChecked); return null; } -} \ No newline at end of file +} diff --git a/src/vs/workbench/browser/parts/editor/textEditor.ts b/src/vs/workbench/browser/parts/editor/textEditor.ts index e1fb9987eb2..f8342b510f5 100644 --- a/src/vs/workbench/browser/parts/editor/textEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textEditor.ts @@ -8,13 +8,12 @@ import * as nls from 'vs/nls'; import { TPromise } from 'vs/base/common/winjs.base'; import URI from 'vs/base/common/uri'; -import { Dimension, Builder } from 'vs/base/browser/builder'; import * as objects from 'vs/base/common/objects'; import * as types from 'vs/base/common/types'; import * as errors from 'vs/base/common/errors'; import * as DOM from 'vs/base/browser/dom'; import { CodeEditor } from 'vs/editor/browser/codeEditor'; -import { EditorInput, EditorOptions } from 'vs/workbench/common/editor'; +import { EditorInput, EditorOptions, EditorViewStateMemento } from 'vs/workbench/common/editor'; import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; import { IEditorViewState, IEditor } from 'vs/editor/common/editorCommon'; import { Position } from 'vs/platform/editor/common/editor'; @@ -43,15 +42,16 @@ export interface IEditorConfiguration { */ export abstract class BaseTextEditor extends BaseEditor { private editorControl: IEditor; - private _editorContainer: Builder; + private _editorContainer: HTMLElement; private hasPendingConfigurationChange: boolean; private lastAppliedEditorOptions: IEditorOptions; + private editorViewStateMemento: EditorViewStateMemento; constructor( id: string, @ITelemetryService telemetryService: ITelemetryService, @IInstantiationService private readonly _instantiationService: IInstantiationService, - @IStorageService private storageService: IStorageService, + @IStorageService storageService: IStorageService, @ITextResourceConfigurationService private readonly _configurationService: ITextResourceConfigurationService, @IThemeService protected themeService: IThemeService, @ITextFileService private readonly _textFileService: ITextFileService, @@ -59,6 +59,8 @@ export abstract class BaseTextEditor extends BaseEditor { ) { super(id, telemetryService, themeService); + this.editorViewStateMemento = new EditorViewStateMemento(this.getMemento(storageService, Scope.WORKSPACE), TEXT_EDITOR_VIEW_STATE_PREFERENCE_KEY, 100); + this.toUnbind.push(this.configurationService.onDidChangeConfiguration(e => this.handleConfigurationChangeEvent(this.configurationService.getValue(this.getResource())))); } @@ -123,7 +125,7 @@ export abstract class BaseTextEditor extends BaseEditor { return overrides; } - protected createEditor(parent: Builder): void { + protected createEditor(parent: HTMLElement): void { // Editor for Text this._editorContainer = parent; @@ -177,10 +179,10 @@ export abstract class BaseTextEditor extends BaseEditor { * * The passed in configuration object should be passed to the editor control when creating it. */ - protected createEditorControl(parent: Builder, configuration: IEditorOptions): IEditor { + protected createEditorControl(parent: HTMLElement, configuration: IEditorOptions): IEditor { // Use a getter for the instantiation service since some subclasses might use scoped instantiation services - return this.instantiationService.createInstance(CodeEditor, parent.getHTMLElement(), configuration); + return this.instantiationService.createInstance(CodeEditor, parent, configuration); } public setInput(input: EditorInput, options?: EditorOptions): TPromise { @@ -189,7 +191,7 @@ export abstract class BaseTextEditor extends BaseEditor { // Update editor options after having set the input. We do this because there can be // editor input specific options (e.g. an ARIA label depending on the input showing) this.updateEditorConfiguration(); - this._editorContainer.getHTMLElement().setAttribute('aria-label', this.computeAriaLabel()); + this._editorContainer.setAttribute('aria-label', this.computeAriaLabel()); }); } @@ -219,7 +221,7 @@ export abstract class BaseTextEditor extends BaseEditor { this.editorControl.focus(); } - public layout(dimension: Dimension): void { + public layout(dimension: DOM.Dimension): void { // Pass on to Editor this.editorControl.layout(dimension); @@ -233,69 +235,51 @@ export abstract class BaseTextEditor extends BaseEditor { * Saves the text editor view state for the given resource. */ protected saveTextEditorViewState(resource: URI): void { + const editorViewState = this.retrieveTextEditorViewState(resource); + if (!editorViewState) { + return; + } + + this.editorViewStateMemento.saveState(resource, this.position, editorViewState); + } + + protected retrieveTextEditorViewState(resource: URI): IEditorViewState { const editor = getCodeOrDiffEditor(this).codeEditor; if (!editor) { - return; // not supported for diff editors + return null; // not supported for diff editors } const model = editor.getModel(); if (!model) { - return; // view state always needs a model + return null; // view state always needs a model } const modelUri = model.uri; if (!modelUri) { - return; // model URI is needed to make sure we save the view state correctly + return null; // model URI is needed to make sure we save the view state correctly } if (modelUri.toString() !== resource.toString()) { - return; // prevent saving view state for a model that is not the expected one + return null; // prevent saving view state for a model that is not the expected one } - const memento = this.getMemento(this.storageService, Scope.WORKSPACE); - - let textEditorViewStateMemento: { [key: string]: { [position: number]: IEditorViewState } } = memento[TEXT_EDITOR_VIEW_STATE_PREFERENCE_KEY]; - if (!textEditorViewStateMemento) { - textEditorViewStateMemento = Object.create(null); - memento[TEXT_EDITOR_VIEW_STATE_PREFERENCE_KEY] = textEditorViewStateMemento; - } - - let lastKnownViewState = textEditorViewStateMemento[resource.toString()]; - if (!lastKnownViewState) { - lastKnownViewState = Object.create(null); - textEditorViewStateMemento[resource.toString()] = lastKnownViewState; - } - - if (typeof this.position === 'number') { - lastKnownViewState[this.position] = editor.saveViewState(); - } + return editor.saveViewState(); } /** * Clears the text editor view state for the given resources. */ protected clearTextEditorViewState(resources: URI[]): void { - const memento = this.getMemento(this.storageService, Scope.WORKSPACE); - const textEditorViewStateMemento: { [key: string]: { [position: number]: IEditorViewState } } = memento[TEXT_EDITOR_VIEW_STATE_PREFERENCE_KEY]; - if (textEditorViewStateMemento) { - resources.forEach(resource => delete textEditorViewStateMemento[resource.toString()]); - } + resources.forEach(resource => { + this.editorViewStateMemento.clearState(resource); + }); } /** * Loads the text editor view state for the given resource and returns it. */ protected loadTextEditorViewState(resource: URI): IEditorViewState { - const memento = this.getMemento(this.storageService, Scope.WORKSPACE); - const textEditorViewStateMemento: { [key: string]: { [position: number]: IEditorViewState } } = memento[TEXT_EDITOR_VIEW_STATE_PREFERENCE_KEY]; - if (textEditorViewStateMemento) { - const viewState = textEditorViewStateMemento[resource.toString()]; - if (viewState) { - return viewState[this.position]; - } - } - - return null; + return this.editorViewStateMemento.loadState(resource, this.position); } private updateEditorConfiguration(configuration = this.configurationService.getValue(this.getResource())): void { @@ -337,6 +321,14 @@ export abstract class BaseTextEditor extends BaseEditor { protected abstract getAriaLabel(): string; + protected saveMemento(): void { + + // ensure to first save our view state memento + this.editorViewStateMemento.save(); + + super.saveMemento(); + } + public dispose(): void { this.lastAppliedEditorOptions = void 0; this.editorControl.dispose(); diff --git a/src/vs/workbench/browser/parts/editor/textResourceEditor.ts b/src/vs/workbench/browser/parts/editor/textResourceEditor.ts index 71f1e85147b..9e4cc6c8799 100644 --- a/src/vs/workbench/browser/parts/editor/textResourceEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textResourceEditor.ts @@ -67,7 +67,7 @@ export class AbstractTextResourceEditor extends BaseTextEditor { } // Remember view settings if input changes - this.saveTextEditorViewStateForInput(this.input); + this.saveTextResourceEditorViewState(this.input); // Set input and resolve return super.setInput(input, options).then(() => { @@ -97,7 +97,7 @@ export class AbstractTextResourceEditor extends BaseTextEditor { // Otherwise restore View State if (!optionsGotApplied) { - this.restoreViewState(input); + this.restoreTextResourceEditorViewState(input); } return void 0; @@ -105,7 +105,7 @@ export class AbstractTextResourceEditor extends BaseTextEditor { }); } - protected restoreViewState(input: EditorInput) { + private restoreTextResourceEditorViewState(input: EditorInput) { if (input instanceof UntitledEditorInput || input instanceof ResourceEditorInput) { const viewState = this.loadTextEditorViewState(input.getResource()); if (viewState) { @@ -153,7 +153,7 @@ export class AbstractTextResourceEditor extends BaseTextEditor { public clearInput(): void { // Keep editor view state in settings to restore when coming back - this.saveTextEditorViewStateForInput(this.input); + this.saveTextResourceEditorViewState(this.input); // Clear Model this.getControl().setModel(null); @@ -165,14 +165,14 @@ export class AbstractTextResourceEditor extends BaseTextEditor { // Save View State (only for untitled) if (this.input instanceof UntitledEditorInput) { - this.saveTextEditorViewStateForInput(this.input); + this.saveTextResourceEditorViewState(this.input); } // Call Super super.shutdown(); } - protected saveTextEditorViewStateForInput(input: EditorInput): void { + private saveTextResourceEditorViewState(input: EditorInput): void { if (!(input instanceof UntitledEditorInput) && !(input instanceof ResourceEditorInput)) { return; // only enabled for untitled and resource inputs } diff --git a/src/vs/workbench/browser/parts/editor/titleControl.ts b/src/vs/workbench/browser/parts/editor/titleControl.ts index edcaa336ae5..d2c4dfd3adb 100644 --- a/src/vs/workbench/browser/parts/editor/titleControl.ts +++ b/src/vs/workbench/browser/parts/editor/titleControl.ts @@ -10,7 +10,6 @@ import * as nls from 'vs/nls'; import { prepareActions } from 'vs/workbench/browser/actions'; import { IAction, Action, IRunEvent } from 'vs/base/common/actions'; import * as errors from 'vs/base/common/errors'; -import * as DOM from 'vs/base/browser/dom'; import { TPromise } from 'vs/base/common/winjs.base'; import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; import { RunOnceScheduler } from 'vs/base/common/async'; @@ -36,8 +35,8 @@ import { ResourceContextKey } from 'vs/workbench/common/resources'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { Themable } from 'vs/workbench/common/theme'; import { isDiffEditor, isCodeEditor } from 'vs/editor/browser/editorBrowser'; -import { Dimension } from 'vs/base/browser/builder'; import { INotificationService } from 'vs/platform/notification/common/notification'; +import { Dimension, findParentWithClass } from 'vs/base/browser/dom'; export interface IToolbarActions { primary: IAction[]; @@ -211,7 +210,7 @@ export abstract class TitleControl extends Themable implements ITitleAreaControl } public allowDragging(element: HTMLElement): boolean { - return !DOM.findParentWithClass(element, 'monaco-action-bar', 'one-editor-silo'); + return !findParentWithClass(element, 'monaco-action-bar', 'one-editor-silo'); } protected initActions(services: IInstantiationService): void { diff --git a/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts b/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts index 008797743bb..50192e62212 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts @@ -10,14 +10,13 @@ import 'vs/css!./media/notificationsActions'; import { Themable, NOTIFICATIONS_BORDER, NOTIFICATIONS_CENTER_HEADER_FOREGROUND, NOTIFICATIONS_CENTER_HEADER_BACKGROUND, NOTIFICATIONS_CENTER_BORDER } from 'vs/workbench/common/theme'; import { IThemeService, registerThemingParticipant, ITheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; import { INotificationsModel, INotificationChangeEvent, NotificationChangeType } from 'vs/workbench/common/notifications'; -import { Dimension } from 'vs/base/browser/builder'; import { IPartService, Parts } from 'vs/workbench/services/part/common/partService'; import { Event, Emitter } from 'vs/base/common/event'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { NotificationsCenterVisibleContext } from 'vs/workbench/browser/parts/notifications/notificationsCommands'; import { NotificationsList } from 'vs/workbench/browser/parts/notifications/notificationsList'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { addClass, removeClass, isAncestor } from 'vs/base/browser/dom'; +import { addClass, removeClass, isAncestor, Dimension } from 'vs/base/browser/dom'; import { widgetShadow } from 'vs/platform/theme/common/colorRegistry'; import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; import { localize } from 'vs/nls'; @@ -290,9 +289,9 @@ export class NotificationsCenter extends Themable { // Hide notifications center first this.hide(); - // Dispose all + // Close all while (this.model.notifications.length) { - this.model.notifications[0].dispose(); + this.model.notifications[0].close(); } } } diff --git a/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts b/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts index 469360901d6..858f3bbe848 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts @@ -115,7 +115,7 @@ export function registerNotificationCommands(center: INotificationsCenterControl handler: (accessor, args?: any) => { const notification = getNotificationFromContext(accessor.get(IListService), args); if (notification) { - notification.dispose(); + notification.close(); } } }); diff --git a/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts b/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts index 6be7e0e3fa5..f52e60460aa 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts @@ -8,10 +8,9 @@ import 'vs/css!./media/notificationsToasts'; import { INotificationsModel, NotificationChangeType, INotificationChangeEvent, INotificationViewItem, NotificationViewItemLabelKind } from 'vs/workbench/common/notifications'; import { IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; -import { addClass, removeClass, isAncestor, addDisposableListener, EventType } from 'vs/base/browser/dom'; +import { addClass, removeClass, isAncestor, addDisposableListener, EventType, Dimension } from 'vs/base/browser/dom'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { NotificationsList } from 'vs/workbench/browser/parts/notifications/notificationsList'; -import { Dimension } from 'vs/base/browser/builder'; import { once } from 'vs/base/common/event'; import { IPartService, Parts } from 'vs/workbench/services/part/common/partService'; import { Themable, NOTIFICATIONS_TOAST_BORDER } from 'vs/workbench/common/theme'; @@ -169,8 +168,8 @@ export class NotificationsToasts extends Themable { } })); - // Remove when item gets disposed - once(item.onDidDispose)(() => { + // Remove when item gets closed + once(item.onDidClose)(() => { this.removeToast(item); }); @@ -437,6 +436,8 @@ export class NotificationsToasts extends Themable { availableHeight -= (2 * 12); // adjust for paddings top and bottom } + availableHeight = Math.round(availableHeight * 0.618); // try to not cover the full height for stacked toasts + return new Dimension(Math.min(maxWidth, availableWidth), availableHeight); } diff --git a/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts b/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts index 41500cf6f61..9cc3d416e22 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts @@ -442,7 +442,7 @@ export class NotificationTemplateRenderer { this.actionRunner.run(action, notification); // Hide notification - notification.dispose(); + notification.close(); })); this.inputDisposeables.push(attachButtonStyler(button, this.themeService)); @@ -456,7 +456,7 @@ export class NotificationTemplateRenderer { // Return early if the item has no progress if (!notification.hasProgress()) { - this.template.progress.stop().getContainer().hide(); + this.template.progress.stop().hide(); return; } @@ -464,23 +464,23 @@ export class NotificationTemplateRenderer { // Infinite const state = notification.progress.state; if (state.infinite) { - this.template.progress.infinite().getContainer().show(); + this.template.progress.infinite().show(); } // Total / Worked - else if (state.total || state.worked) { - if (state.total) { + else if (typeof state.total === 'number' || typeof state.worked === 'number') { + if (typeof state.total === 'number' && !this.template.progress.hasTotal()) { this.template.progress.total(state.total); } - if (state.worked) { - this.template.progress.worked(state.worked).getContainer().show(); + if (typeof state.worked === 'number') { + this.template.progress.worked(state.worked).show(); } } // Done else { - this.template.progress.done().getContainer().hide(); + this.template.progress.done().hide(); } } diff --git a/src/vs/workbench/browser/parts/panel/media/panelpart.css b/src/vs/workbench/browser/parts/panel/media/panelpart.css index fc207f64896..4b0a0a4c7e3 100644 --- a/src/vs/workbench/browser/parts/panel/media/panelpart.css +++ b/src/vs/workbench/browser/parts/panel/media/panelpart.css @@ -30,10 +30,6 @@ border-left-style: solid; } -.monaco-workbench > .part.panel > .composite.title > .title-actions { - flex-grow: 0; -} - .monaco-workbench > .part.panel > .title > .title-actions .monaco-action-bar .action-item .action-label { outline-offset: -2px; } diff --git a/src/vs/workbench/browser/parts/panel/panelPart.ts b/src/vs/workbench/browser/parts/panel/panelPart.ts index e7ff7537134..d339b0cfe04 100644 --- a/src/vs/workbench/browser/parts/panel/panelPart.ts +++ b/src/vs/workbench/browser/parts/panel/panelPart.ts @@ -7,7 +7,7 @@ import 'vs/css!./media/panelpart'; import { TPromise } from 'vs/base/common/winjs.base'; import { IAction, Action } from 'vs/base/common/actions'; import { Event } from 'vs/base/common/event'; -import { Builder, Dimension } from 'vs/base/browser/builder'; +import { $ } from 'vs/base/browser/builder'; import { Registry } from 'vs/platform/registry/common/platform'; import { ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar'; import { IPanel } from 'vs/workbench/common/panel'; @@ -30,6 +30,7 @@ import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { dispose, IDisposable } from 'vs/base/common/lifecycle'; import { IBadge } from 'vs/workbench/services/activity/common/activity'; import { INotificationService } from 'vs/platform/notification/common/notification'; +import { Dimension } from 'vs/base/browser/dom'; export class PanelPart extends CompositePart implements IPanelService { @@ -42,7 +43,6 @@ export class PanelPart extends CompositePart implements IPanelService { private blockOpeningPanel: boolean; private compositeBar: CompositeBar; private dimension: Dimension; - private toolbarWidth = new Map(); constructor( id: string, @@ -100,6 +100,8 @@ export class PanelPart extends CompositePart implements IPanelService { private registerListeners(): void { + this.toUnbind.push(this.registry.onDidRegister(panelDescriptor => this.compositeBar.addComposite(panelDescriptor, false))); + // Activate panel action on opening of a panel this.toUnbind.push(this.onDidPanelOpen(panel => { this.compositeBar.activateComposite(panel.getId()); @@ -123,11 +125,11 @@ export class PanelPart extends CompositePart implements IPanelService { public updateStyles(): void { super.updateStyles(); - const container = this.getContainer(); + const container = $(this.getContainer()); container.style('background-color', this.getColor(PANEL_BACKGROUND)); container.style('border-left-color', this.getColor(PANEL_BORDER) || this.getColor(contrastBorder)); - const title = this.getTitleArea(); + const title = $(this.getTitleArea()); title.style('border-top-color', this.getColor(PANEL_BORDER) || this.getColor(contrastBorder)); } @@ -180,7 +182,7 @@ export class PanelPart extends CompositePart implements IPanelService { if (descriptor && descriptor.enabled !== enabled) { descriptor.enabled = enabled; if (enabled) { - this.compositeBar.addComposite(descriptor); + this.compositeBar.addComposite(descriptor, true); } else { this.compositeBar.removeComposite(id); } @@ -207,8 +209,8 @@ export class PanelPart extends CompositePart implements IPanelService { return this.hideActiveComposite().then(composite => void 0); } - protected createTitleLabel(parent: Builder): ICompositeTitleLabel { - const titleArea = this.compositeBar.create(parent.getHTMLElement()); + protected createTitleLabel(parent: HTMLElement): ICompositeTitleLabel { + const titleArea = this.compositeBar.create(parent); titleArea.classList.add('panel-switcher-container'); return { @@ -241,6 +243,11 @@ export class PanelPart extends CompositePart implements IPanelService { return sizes; } + public shutdown(): void { + this.compositeBar.shutdown(); + super.shutdown(); + } + private layoutCompositeBar(): void { if (this.dimension) { let availableWidth = this.dimension.width - 40; // take padding into account @@ -257,11 +264,7 @@ export class PanelPart extends CompositePart implements IPanelService { if (!activePanel) { return 0; } - if (!this.toolbarWidth.has(activePanel.getId())) { - this.toolbarWidth.set(activePanel.getId(), this.toolBar.getContainer().getHTMLElement().offsetWidth); - } - - return this.toolbarWidth.get(activePanel.getId()); + return this.toolBar.getItemsWidth(); } } diff --git a/src/vs/workbench/browser/parts/quickinput/quickInput.css b/src/vs/workbench/browser/parts/quickinput/quickInput.css new file mode 100644 index 00000000000..28ec8b5c3dd --- /dev/null +++ b/src/vs/workbench/browser/parts/quickinput/quickInput.css @@ -0,0 +1,113 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.quick-input-widget { + position: absolute; + width: 600px; + z-index: 2000; + padding-bottom: 6px; + left: 50%; + margin-left: -300px; +} + +.quick-input-header { + display: flex; + padding: 6px 6px 4px 6px; +} + +.quick-input-check-all { + align-self: center; + margin: 0; +} + +.quick-input-filter { + flex-grow: 1; + display: flex; + position: relative; +} + +.quick-input-box { + flex-grow: 1; + margin-left: 5px; +} + +.quick-input-count { + align-self: center; + position: absolute; + right: 4px; +} + +.quick-input-count .monaco-count-badge { + vertical-align: middle; +} + +.quick-input-action { + margin-left: 6px; +} + +.quick-input-action .monaco-text-button { + font-size: 85%; + padding: 7px 6px 6px 6px; + line-height: initial; +} + +.quick-input-progress.monaco-progress-container, +.quick-input-progress.monaco-progress-container .progress-bit { + height: 2px; +} + +.quick-input-checkbox-list { + line-height: 22px; +} + +.quick-input-checkbox-list .monaco-list { + overflow: hidden; + max-height: calc(20 * 22px); +} + +.quick-input-checkbox-list .quick-input-checkbox-list-entry { + overflow: hidden; + display: flex; + height: 100%; + padding: 0 6px; +} + +.quick-input-checkbox-list .quick-input-checkbox-list-label { + overflow: hidden; + display: flex; + height: 100%; + flex: 1; +} + +.quick-input-checkbox-list .quick-input-checkbox-list-checkbox { + align-self: center; + margin: 0; + /* TODO */ + /* margin-top: 5px; */ +} + +.quick-input-checkbox-list .quick-input-checkbox-list-rows { + overflow: hidden; + text-overflow: ellipsis; + display: flex; + flex-direction: column; + height: 100%; + flex: 1; + margin-left: 10px; +} + +.quick-input-checkbox-list .quick-input-checkbox-list-rows > .quick-input-checkbox-list-row { + display: flex; + align-items: center; +} + +.quick-input-checkbox-list .quick-input-checkbox-list-rows .monaco-highlighted-label span { + opacity: 1; +} + +.quick-input-checkbox-list .quick-input-checkbox-list-label-meta { + opacity: 0.7; + line-height: normal; +} diff --git a/src/vs/workbench/browser/parts/quickinput/quickInput.ts b/src/vs/workbench/browser/parts/quickinput/quickInput.ts new file mode 100644 index 00000000000..8d4d6642e47 --- /dev/null +++ b/src/vs/workbench/browser/parts/quickinput/quickInput.ts @@ -0,0 +1,297 @@ +/*--------------------------------------------------------------------------------------------- + * 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!./quickInput'; +import { Component } from 'vs/workbench/common/component'; +import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; +import { IPartService } from 'vs/workbench/services/part/common/partService'; +import * as dom from 'vs/base/browser/dom'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { contrastBorder, widgetShadow } from 'vs/platform/theme/common/colorRegistry'; +import { SIDE_BAR_BACKGROUND, SIDE_BAR_FOREGROUND } from 'vs/workbench/common/theme'; +import { IQuickOpenService, IPickOpenEntry, IPickOptions } from 'vs/platform/quickOpen/common/quickOpen'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { QuickInputCheckboxList } from './quickInputCheckboxList'; +import { QuickInputBox } from './quickInputBox'; +import { KeyCode } from 'vs/base/common/keyCodes'; +import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { localize } from 'vs/nls'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { CLOSE_ON_FOCUS_LOST_CONFIG } from 'vs/workbench/browser/quickopen'; +import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge'; +import { attachBadgeStyler, attachProgressBarStyler, attachButtonStyler } from 'vs/platform/theme/common/styler'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar'; +import { chain } from 'vs/base/common/event'; +import { Button } from 'vs/base/browser/ui/button/button'; + +const $ = dom.$; + +export class QuickInputService extends Component implements IQuickInputService { + + public _serviceBrand: any; + + private static readonly ID = 'workbench.component.quickinput'; + private static readonly MAX_WIDTH = 600; // Max total width of quick open widget + + private layoutDimensions: dom.Dimension; + private container: HTMLElement; + private checkAll: HTMLInputElement; + private inputBox: QuickInputBox; + private count: CountBadge; + private ready = false; + private progressBar: ProgressBar; + private checkboxList: QuickInputCheckboxList; + private ignoreFocusLost = false; + + private resolve: (value?: IPickOpenEntry[] | Thenable) => void; + private progress: (value: IPickOpenEntry) => void; + + constructor( + @IEnvironmentService private environmentService: IEnvironmentService, + @IConfigurationService private configurationService: IConfigurationService, + @IInstantiationService private instantiationService: IInstantiationService, + @IPartService private partService: IPartService, + @IQuickOpenService private quickOpenService: IQuickOpenService, + @IThemeService themeService: IThemeService + ) { + super(QuickInputService.ID, themeService); + } + + private create() { + if (this.container) { + return; + } + + const workbench = document.getElementById(this.partService.getWorkbenchElementId()); + this.container = dom.append(workbench, $('.quick-input-widget')); + this.container.tabIndex = -1; + this.container.style.display = 'none'; + + const headerContainer = dom.append(this.container, $('.quick-input-header')); + + this.checkAll = dom.append(headerContainer, $('input.quick-input-check-all')); + this.checkAll.type = 'checkbox'; + this.toUnbind.push(dom.addStandardDisposableListener(this.checkAll, dom.EventType.CHANGE, e => { + const checked = this.checkAll.checked; + this.checkboxList.setAllVisibleChecked(checked); + })); + this.toUnbind.push(dom.addDisposableListener(this.checkAll, dom.EventType.CLICK, e => { + if (e.x || e.y) { // Avoid 'click' triggered by 'space'... + this.inputBox.setFocus(); + } + })); + + const filterContainer = dom.append(headerContainer, $('.quick-input-filter')); + + this.inputBox = new QuickInputBox(filterContainer); + this.toUnbind.push(this.inputBox); + this.inputBox.onDidChange(value => { + this.checkboxList.filter(value); + }); + this.toUnbind.push(this.inputBox.onKeyDown(event => { + switch (event.keyCode) { + case KeyCode.DownArrow: + this.checkboxList.focus('First'); + this.checkboxList.domFocus(); + break; + case KeyCode.UpArrow: + this.checkboxList.focus('Last'); + this.checkboxList.domFocus(); + break; + } + })); + + const badgeContainer = dom.append(filterContainer, $('.quick-input-count')); + this.count = new CountBadge(badgeContainer, { countFormat: localize('quickInput.countSelected', "{0} Selected") }); + this.toUnbind.push(attachBadgeStyler(this.count, this.themeService)); + + const okContainer = dom.append(headerContainer, $('.quick-input-action')); + const ok = new Button(okContainer); + attachButtonStyler(ok, this.themeService); + ok.label = localize('ok', "OK"); + this.toUnbind.push(ok.onDidClick(e => { + if (this.ready) { + this.close(this.checkboxList.getCheckedElements()); + } + })); + + this.progressBar = new ProgressBar(this.container); + dom.addClass(this.progressBar.getContainer(), 'quick-input-progress'); + this.toUnbind.push(attachProgressBarStyler(this.progressBar, this.themeService)); + + this.checkboxList = this.instantiationService.createInstance(QuickInputCheckboxList, this.container); + this.toUnbind.push(this.checkboxList); + this.toUnbind.push(this.checkboxList.onAllVisibleCheckedChanged(checked => { + this.checkAll.checked = checked; + })); + this.toUnbind.push(this.checkboxList.onCheckedCountChanged(count => { + this.count.setCount(count); + })); + this.toUnbind.push(this.checkboxList.onLeave(() => { + // Defer to avoid the input field reacting to the triggering key. + setTimeout(() => { + this.inputBox.setFocus(); + this.checkboxList.clearFocus(); + }, 0); + })); + this.toUnbind.push( + chain(this.checkboxList.onFocusChange) + .map(e => e[0]) + .filter(e => !!e) + .latch() + .on(e => this.progress && this.progress(e)) + ); + + this.toUnbind.push(dom.addDisposableListener(this.container, 'focusout', (e: FocusEvent) => { + if (e.relatedTarget === this.container) { + (e.target).focus(); + return; + } + for (let element = e.relatedTarget; element; element = element.parentElement) { + if (element === this.container) { + return; + } + } + if (!this.ignoreFocusLost && !this.environmentService.args['sticky-quickopen'] && this.configurationService.getValue(CLOSE_ON_FOCUS_LOST_CONFIG)) { + this.close(); + } + })); + this.toUnbind.push(dom.addDisposableListener(this.container, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => { + const event = new StandardKeyboardEvent(e); + switch (event.keyCode) { + case KeyCode.Enter: + if (this.ready) { + dom.EventHelper.stop(e, true); + this.close(this.checkboxList.getCheckedElements()); + } + break; + case KeyCode.Escape: + dom.EventHelper.stop(e, true); + this.close(); + break; + case KeyCode.Tab: + if (!event.altKey && !event.ctrlKey && !event.metaKey) { + const inputs = this.container.querySelectorAll('input'); + if (event.shiftKey && event.target === inputs[0]) { + dom.EventHelper.stop(e, true); + inputs[inputs.length - 1].focus(); + } else if (!event.shiftKey && event.target === inputs[inputs.length - 1]) { + dom.EventHelper.stop(e, true); + inputs[0].focus(); + } + } + break; + } + })); + + this.toUnbind.push(this.quickOpenService.onShow(() => this.close())); + + this.updateStyles(); + } + + private close(value?: IPickOpenEntry[] | Thenable) { + if (this.resolve) { + this.resolve(value); + } + this.container.style.display = 'none'; + } + + pick(picks: TPromise, options: IPickOptions = {}, token: CancellationToken = CancellationToken.None): TPromise { + this.create(); + this.quickOpenService.close(); + if (this.resolve) { + this.resolve(); + } + + this.inputBox.value = ''; + this.inputBox.setPlaceholder(options.placeHolder || ''); + this.checkboxList.matchOnDescription = options.matchOnDescription; + this.checkboxList.matchOnDetail = options.matchOnDetail; + this.ignoreFocusLost = options.ignoreFocusLost; + + this.progressBar.stop(); + this.ready = false; + + this.checkboxList.setElements([]); + this.checkAll.checked = this.checkboxList.getAllVisibleChecked(); + this.count.setCount(this.checkboxList.getCheckedCount()); + + this.container.style.display = null; + this.updateLayout(); + this.inputBox.setFocus(); + + const result = new TPromise((resolve, reject, progress) => { + this.resolve = resolve; + this.progress = progress; + }); + const d = token.onCancellationRequested(() => this.close()); + result.then(() => d.dispose(), () => d.dispose()); + + const delay = TPromise.timeout(800); + delay.then(() => this.progressBar.infinite(), () => { /* ignore */ }); + + const wasResolve = this.resolve; + picks.then(elements => { + delay.cancel(); + if (this.resolve !== wasResolve) { + return; + } + + this.progressBar.stop(); + this.ready = true; + + this.checkboxList.setElements(elements); + this.checkboxList.filter(this.inputBox.value); + this.checkAll.checked = this.checkboxList.getAllVisibleChecked(); + this.count.setCount(this.checkboxList.getCheckedCount()); + + this.updateLayout(); + }).then(null, reason => this.close(TPromise.wrapError(reason))); + + return result; + } + + public layout(dimension: dom.Dimension): void { + this.layoutDimensions = dimension; + this.updateLayout(); + } + + private updateLayout() { + if (this.layoutDimensions && this.container) { + const titlebarOffset = this.partService.getTitleBarOffset(); + this.container.style.top = `${titlebarOffset}px`; + + const style = this.container.style; + const width = Math.min(this.layoutDimensions.width * 0.62 /* golden cut */, QuickInputService.MAX_WIDTH); + style.width = width + 'px'; + style.marginLeft = '-' + (width / 2) + 'px'; + + this.inputBox.layout(); + this.checkboxList.layout(); + } + } + + protected updateStyles() { + const theme = this.themeService.getTheme(); + if (this.inputBox) { + this.inputBox.style(theme); + } + if (this.container) { + const sideBarBackground = theme.getColor(SIDE_BAR_BACKGROUND); + this.container.style.backgroundColor = sideBarBackground ? sideBarBackground.toString() : undefined; + const sideBarForeground = theme.getColor(SIDE_BAR_FOREGROUND); + this.container.style.color = sideBarForeground ? sideBarForeground.toString() : undefined; + const contrastBorderColor = theme.getColor(contrastBorder); + this.container.style.border = contrastBorderColor ? `1px solid ${contrastBorderColor}` : undefined; + const widgetShadowColor = theme.getColor(widgetShadow); + this.container.style.boxShadow = widgetShadowColor ? `0 5px 8px ${widgetShadowColor}` : undefined; + } + } +} diff --git a/src/vs/workbench/browser/parts/quickinput/quickInputBox.ts b/src/vs/workbench/browser/parts/quickinput/quickInputBox.ts new file mode 100644 index 00000000000..0acd72de5f9 --- /dev/null +++ b/src/vs/workbench/browser/parts/quickinput/quickInputBox.ts @@ -0,0 +1,84 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import 'vs/css!./quickInput'; +import * as dom from 'vs/base/browser/dom'; +import { InputBox } from 'vs/base/browser/ui/inputbox/inputBox'; +import { localize } from 'vs/nls'; +import { inputBackground, inputForeground, inputBorder } from 'vs/platform/theme/common/colorRegistry'; +import { ITheme } from 'vs/platform/theme/common/themeService'; +import { dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; + +const $ = dom.$; + +const DEFAULT_INPUT_ARIA_LABEL = localize('quickInputBox.ariaLabel', "Type to narrow down results."); + +export class QuickInputBox { + + private container: HTMLElement; + private inputBox: InputBox; + private disposables: IDisposable[] = []; + + constructor( + private parent: HTMLElement + ) { + this.container = dom.append(this.parent, $('.quick-input-box')); + this.inputBox = new InputBox(this.container, null, { + ariaLabel: DEFAULT_INPUT_ARIA_LABEL + }); + this.disposables.push(this.inputBox); + + // ARIA + const inputElement = this.inputBox.inputElement; + inputElement.setAttribute('role', 'combobox'); + inputElement.setAttribute('aria-haspopup', 'false'); + inputElement.setAttribute('aria-autocomplete', 'list'); + } + + onKeyDown(handler: (event: StandardKeyboardEvent) => void): IDisposable { + return dom.addDisposableListener(this.inputBox.inputElement, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => { + handler(new StandardKeyboardEvent(e)); + }); + } + + onDidChange(handler: (event: string) => void): IDisposable { + return this.inputBox.onDidChange(handler); + } + + get value() { + return this.inputBox.value; + } + + set value(value: string) { + this.inputBox.value = value; + } + + setPlaceholder(placeholder: string) { + this.inputBox.setPlaceHolder(placeholder); + } + + setFocus(): void { + this.inputBox.focus(); + } + + layout(): void { + this.inputBox.layout(); + } + + style(theme: ITheme) { + this.inputBox.style({ + inputForeground: theme.getColor(inputForeground), + inputBackground: theme.getColor(inputBackground), + inputBorder: theme.getColor(inputBorder) + }); + } + + dispose() { + this.disposables = dispose(this.disposables); + } +} diff --git a/src/vs/workbench/browser/parts/quickinput/quickInputCheckboxList.ts b/src/vs/workbench/browser/parts/quickinput/quickInputCheckboxList.ts new file mode 100644 index 00000000000..fa0763bf1af --- /dev/null +++ b/src/vs/workbench/browser/parts/quickinput/quickInputCheckboxList.ts @@ -0,0 +1,381 @@ +/*--------------------------------------------------------------------------------------------- + * 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!./quickInput'; +import { IDelegate, IRenderer } from 'vs/base/browser/ui/list/list'; +import * as dom from 'vs/base/browser/dom'; +import { dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { WorkbenchList } from 'vs/platform/list/browser/listService'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IPickOpenEntry } from 'vs/platform/quickOpen/common/quickOpen'; +import { IMatch } from 'vs/base/common/filters'; +import { matchesFuzzyOcticonAware, parseOcticons } from 'vs/base/common/octicon'; +import { compareAnything } from 'vs/base/common/comparers'; +import { Emitter, Event, mapEvent } from 'vs/base/common/event'; +import { assign } from 'vs/base/common/objects'; +import { KeyCode } from 'vs/base/common/keyCodes'; +import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { IconLabel, IIconLabelValueOptions } from 'vs/base/browser/ui/iconLabel/iconLabel'; +import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; +import { memoize } from 'vs/base/common/decorators'; +import { range } from 'vs/base/common/arrays'; +import * as platform from 'vs/base/common/platform'; + +const $ = dom.$; + +interface ICheckableElement { + index: number; + item: IPickOpenEntry; + checked: boolean; +} + +class CheckableElement implements ICheckableElement { + index: number; + item: IPickOpenEntry; + shouldAlwaysShow = false; + hidden = false; + private _onChecked = new Emitter(); + onChecked = this._onChecked.event; + _checked: boolean; + get checked() { + return this._checked; + } + set checked(value: boolean) { + if (value !== this._checked) { + this._checked = value; + this._onChecked.fire(value); + } + } + labelHighlights?: IMatch[]; + descriptionHighlights?: IMatch[]; + detailHighlights?: IMatch[]; + + constructor(init: ICheckableElement) { + assign(this, init); + } +} + +interface ICheckableElementTemplateData { + checkbox: HTMLInputElement; + label: IconLabel; + detail: HighlightedLabel; + element: CheckableElement; + toDisposeElement: IDisposable[]; + toDisposeTemplate: IDisposable[]; +} + +class CheckableElementRenderer implements IRenderer { + + static readonly ID = 'checkableelement'; + + get templateId() { + return CheckableElementRenderer.ID; + } + + renderTemplate(container: HTMLElement): ICheckableElementTemplateData { + const data: ICheckableElementTemplateData = Object.create(null); + + const entry = dom.append(container, $('.quick-input-checkbox-list-entry')); + const label = dom.append(entry, $('label.quick-input-checkbox-list-label')); + + // Entry + data.checkbox = dom.append(label, $('input.quick-input-checkbox-list-checkbox')); + data.checkbox.type = 'checkbox'; + data.toDisposeElement = []; + data.toDisposeTemplate = []; + data.toDisposeTemplate.push(dom.addStandardDisposableListener(data.checkbox, dom.EventType.CHANGE, e => { + data.element.checked = data.checkbox.checked; + })); + + const rows = dom.append(label, $('.quick-input-checkbox-list-rows')); + const row1 = dom.append(rows, $('.quick-input-checkbox-list-row')); + const row2 = dom.append(rows, $('.quick-input-checkbox-list-row')); + + // Label + data.label = new IconLabel(row1, { supportHighlights: true, supportDescriptionHighlights: true }); + + // Detail + const detailContainer = dom.append(row2, $('.quick-input-checkbox-list-label-meta')); + data.detail = new HighlightedLabel(detailContainer); + + return data; + } + + renderElement(element: CheckableElement, index: number, data: ICheckableElementTemplateData): void { + data.toDisposeElement = dispose(data.toDisposeElement); + data.element = element; + data.checkbox.checked = element.checked; + data.toDisposeElement.push(element.onChecked(checked => data.checkbox.checked = checked)); + + const { labelHighlights, descriptionHighlights, detailHighlights } = element; + + // Label + const options: IIconLabelValueOptions = Object.create(null); + options.matches = labelHighlights || []; + options.descriptionTitle = element.item.description; + options.descriptionMatches = descriptionHighlights || []; + data.label.setValue(element.item.label, element.item.description, options); + + // Meta + data.detail.set(element.item.detail, detailHighlights); + } + + disposeTemplate(data: ICheckableElementTemplateData): void { + data.toDisposeElement = dispose(data.toDisposeElement); + data.toDisposeTemplate = dispose(data.toDisposeTemplate); + } +} + +class CheckableElementDelegate implements IDelegate { + + getHeight(element: CheckableElement): number { + return element.item.detail ? 44 : 22; + } + + getTemplateId(element: CheckableElement): string { + return CheckableElementRenderer.ID; + } +} + +export class QuickInputCheckboxList { + + private container: HTMLElement; + private list: WorkbenchList; + private elements: CheckableElement[] = []; + matchOnDescription = false; + matchOnDetail = false; + private _onAllVisibleCheckedChanged = new Emitter(); // TODO: Debounce + onAllVisibleCheckedChanged: Event = this._onAllVisibleCheckedChanged.event; + private _onCheckedCountChanged = new Emitter(); // TODO: Debounce + onCheckedCountChanged: Event = this._onCheckedCountChanged.event; + private _onLeave = new Emitter(); + onLeave: Event = this._onLeave.event; + private _fireCheckedEvents = true; + private elementDisposables: IDisposable[] = []; + private disposables: IDisposable[] = []; + + constructor( + private parent: HTMLElement, + @IInstantiationService private instantiationService: IInstantiationService + ) { + this.container = dom.append(this.parent, $('.quick-input-checkbox-list')); + const delegate = new CheckableElementDelegate(); + this.list = this.instantiationService.createInstance(WorkbenchList, this.container, delegate, [new CheckableElementRenderer()], { + identityProvider: element => element.label, + multipleSelectionSupport: false + }) as WorkbenchList; + this.disposables.push(this.list); + this.disposables.push(this.list.onKeyDown(e => { + const event = new StandardKeyboardEvent(e); + switch (event.keyCode) { + case KeyCode.Space: + this.toggleCheckbox(); + break; + case KeyCode.KEY_A: + if (platform.isMacintosh ? e.metaKey : e.ctrlKey) { + this.list.setFocus(range(this.list.length)); + } + break; + case KeyCode.UpArrow: + const focus1 = this.list.getFocus(); + if (focus1.length === 1 && focus1[0] === 0) { + this._onLeave.fire(); + } + break; + case KeyCode.DownArrow: + const focus2 = this.list.getFocus(); + if (focus2.length === 1 && focus2[0] === this.list.length - 1) { + this._onLeave.fire(); + } + break; + } + })); + this.disposables.push(dom.addDisposableListener(this.container, dom.EventType.CLICK, e => { + if (e.x || e.y) { // Avoid 'click' triggered by 'space' on checkbox. + this._onLeave.fire(); + } + })); + this.disposables.push(this.list.onSelectionChange(e => { + if (e.elements.length) { + this.list.setSelection([]); + } + })); + } + + @memoize + get onFocusChange() { + return mapEvent(this.list.onFocusChange, e => e.elements.map(e => e.item)); + } + + getAllVisibleChecked() { + return this.allVisibleChecked(this.elements, false); + } + + private allVisibleChecked(elements: CheckableElement[], whenNoneVisible = true) { + for (let i = 0, n = elements.length; i < n; i++) { + const element = elements[i]; + if (!element.hidden) { + if (!element.checked) { + return false; + } else { + whenNoneVisible = true; + } + } + } + return whenNoneVisible; + } + + getCheckedCount() { + let count = 0; + const elements = this.elements; + for (let i = 0, n = elements.length; i < n; i++) { + if (elements[i].checked) { + count++; + } + } + return count; + } + + setAllVisibleChecked(checked: boolean) { + try { + this._fireCheckedEvents = false; + this.elements.forEach(element => { + if (!element.hidden) { + element.checked = checked; + } + }); + } finally { + this._fireCheckedEvents = true; + this.fireCheckedEvents(); + } + } + + setElements(elements: IPickOpenEntry[]): void { + this.elementDisposables = dispose(this.elementDisposables); + this.elements = elements.map((item, index) => new CheckableElement({ + index, + item, + checked: !!item.picked + })); + this.elementDisposables.push(...this.elements.map(element => element.onChecked(() => this.fireCheckedEvents()))); + this.list.splice(0, this.list.length, this.elements); + this.list.setFocus([]); + } + + getCheckedElements() { + return this.elements.filter(e => e.checked) + .map(e => e.item); + } + + focus(what: 'First' | 'Last' | 'Next' | 'Previous' | 'NextPage' | 'PreviousPage'): void { + this.list['focus' + what](); + this.list.reveal(this.list.getFocus()[0]); + } + + clearFocus() { + this.list.setFocus([]); + } + + domFocus() { + this.list.domFocus(); + } + + layout(): void { + this.list.layout(); + } + + filter(query: string) { + query = query.trim(); + + // Reset filtering + if (!query) { + this.elements.forEach(element => { + element.labelHighlights = undefined; + element.descriptionHighlights = undefined; + element.detailHighlights = undefined; + element.hidden = false; + }); + } + + // Filter by value (since we support octicons, use octicon aware fuzzy matching) + else { + this.elements.forEach(element => { + const labelHighlights = matchesFuzzyOcticonAware(query, parseOcticons(element.item.label)); + const descriptionHighlights = this.matchOnDescription ? matchesFuzzyOcticonAware(query, parseOcticons(element.item.description || '')) : undefined; + const detailHighlights = this.matchOnDetail ? matchesFuzzyOcticonAware(query, parseOcticons(element.item.detail || '')) : undefined; + + if (element.shouldAlwaysShow || labelHighlights || descriptionHighlights || detailHighlights) { + element.labelHighlights = labelHighlights; + element.descriptionHighlights = descriptionHighlights; + element.detailHighlights = detailHighlights; + element.hidden = false; + } else { + element.labelHighlights = undefined; + element.descriptionHighlights = undefined; + element.detailHighlights = undefined; + element.hidden = true; + } + }); + } + + // Sort by value + const normalizedSearchValue = query.toLowerCase(); + this.elements.sort((a, b) => { + if (!query) { + return a.index - b.index; // restore natural order + } + return compareEntries(a, b, normalizedSearchValue); + }); + + this.list.splice(0, this.list.length, this.elements.filter(element => !element.hidden)); + this.list.setFocus([]); + this.list.layout(); + + this._onAllVisibleCheckedChanged.fire(this.getAllVisibleChecked()); + } + + toggleCheckbox() { + try { + this._fireCheckedEvents = false; + const elements = this.list.getFocusedElements(); + const allChecked = this.allVisibleChecked(elements); + for (const element of elements) { + element.checked = !allChecked; + } + } finally { + this._fireCheckedEvents = true; + this.fireCheckedEvents(); + } + } + + dispose() { + this.elementDisposables = dispose(this.elementDisposables); + this.disposables = dispose(this.disposables); + } + + private fireCheckedEvents() { + if (this._fireCheckedEvents) { + this._onAllVisibleCheckedChanged.fire(this.getAllVisibleChecked()); + this._onCheckedCountChanged.fire(this.getCheckedCount()); + } + } +} + +function compareEntries(elementA: CheckableElement, elementB: CheckableElement, lookFor: string): number { + + const labelHighlightsA = elementA.labelHighlights || []; + const labelHighlightsB = elementB.labelHighlights || []; + if (labelHighlightsA.length && !labelHighlightsB.length) { + return -1; + } + + if (!labelHighlightsA.length && labelHighlightsB.length) { + return 1; + } + + return compareAnything(elementA.item.label, elementB.item.label, lookFor); +} \ No newline at end of file diff --git a/src/vs/workbench/browser/parts/quickopen/quickOpenController.ts b/src/vs/workbench/browser/parts/quickopen/quickOpenController.ts index 28896b52a38..da5281628c2 100644 --- a/src/vs/workbench/browser/parts/quickopen/quickOpenController.ts +++ b/src/vs/workbench/browser/parts/quickopen/quickOpenController.ts @@ -9,9 +9,7 @@ import 'vs/css!./media/quickopen'; import { TPromise, ValueCallback } from 'vs/base/common/winjs.base'; import * as nls from 'vs/nls'; import * as browser from 'vs/base/browser/browser'; -import { Dimension, withElementById } from 'vs/base/browser/builder'; import * as strings from 'vs/base/common/strings'; -import * as DOM from 'vs/base/browser/dom'; import URI from 'vs/base/common/uri'; import * as resources from 'vs/base/common/resources'; import { defaultGenerator } from 'vs/base/common/idGenerator'; @@ -57,6 +55,7 @@ import { IMatch } from 'vs/base/common/filters'; import { Schemas } from 'vs/base/common/network'; import Severity from 'vs/base/common/severity'; import { INotificationService } from 'vs/platform/notification/common/notification'; +import { Dimension, addClass } from 'vs/base/browser/dom'; const HELP_PREFIX = '?'; @@ -302,7 +301,7 @@ export class QuickOpenController extends Component implements IQuickOpenService // Create upon first open if (!this.pickOpenWidget) { this.pickOpenWidget = new QuickOpenWidget( - withElementById(this.partService.getWorkbenchElementId()).getHTMLElement(), + document.getElementById(this.partService.getWorkbenchElementId()), { onOk: () => { /* ignore, handle later */ }, onCancel: () => { /* ignore, handle later */ }, @@ -318,7 +317,7 @@ export class QuickOpenController extends Component implements IQuickOpenService this.toUnbind.push(attachQuickOpenStyler(this.pickOpenWidget, this.themeService, { background: SIDE_BAR_BACKGROUND, foreground: SIDE_BAR_FOREGROUND })); const pickOpenContainer = this.pickOpenWidget.create(); - DOM.addClass(pickOpenContainer, 'show-file-icons'); + addClass(pickOpenContainer, 'show-file-icons'); this.positionQuickOpenWidget(); } @@ -374,7 +373,7 @@ export class QuickOpenController extends Component implements IQuickOpenService picksPromiseDone = true; // Reset Progress - this.pickOpenWidget.getProgressBar().stop().getContainer().hide(); + this.pickOpenWidget.getProgressBar().stop().hide(); // Model const model = new QuickOpenModel([], new PickOpenActionProvider()); @@ -487,7 +486,7 @@ export class QuickOpenController extends Component implements IQuickOpenService // Progress if task takes a long time TPromise.timeout(800).then(() => { if (!picksPromiseDone && this.currentPickerToken === currentPickerToken) { - this.pickOpenWidget.getProgressBar().infinite().getContainer().show(); + this.pickOpenWidget.getProgressBar().infinite().show(); } }); @@ -557,7 +556,7 @@ export class QuickOpenController extends Component implements IQuickOpenService // Create upon first open if (!this.quickOpenWidget) { this.quickOpenWidget = new QuickOpenWidget( - withElementById(this.partService.getWorkbenchElementId()).getHTMLElement(), + document.getElementById(this.partService.getWorkbenchElementId()), { onOk: () => { /* ignore */ }, onCancel: () => { /* ignore */ }, @@ -574,7 +573,7 @@ export class QuickOpenController extends Component implements IQuickOpenService this.toUnbind.push(attachQuickOpenStyler(this.quickOpenWidget, this.themeService, { background: SIDE_BAR_BACKGROUND, foreground: SIDE_BAR_FOREGROUND })); const quickOpenContainer = this.quickOpenWidget.create(); - DOM.addClass(quickOpenContainer, 'show-file-icons'); + addClass(quickOpenContainer, 'show-file-icons'); this.positionQuickOpenWidget(); } @@ -623,11 +622,11 @@ export class QuickOpenController extends Component implements IQuickOpenService const titlebarOffset = this.partService.getTitleBarOffset(); if (this.quickOpenWidget) { - this.quickOpenWidget.getElement().style('top', `${titlebarOffset}px`); + this.quickOpenWidget.getElement().style.top = `${titlebarOffset}px`; } if (this.pickOpenWidget) { - this.pickOpenWidget.getElement().style('top', `${titlebarOffset}px`); + this.pickOpenWidget.getElement().style.top = `${titlebarOffset}px`; } } @@ -742,7 +741,7 @@ export class QuickOpenController extends Component implements IQuickOpenService // Reset Progress if (!instantProgress) { - this.quickOpenWidget.getProgressBar().stop().getContainer().hide(); + this.quickOpenWidget.getProgressBar().stop().hide(); } // Reset Extra Class @@ -784,7 +783,7 @@ export class QuickOpenController extends Component implements IQuickOpenService // Progress if task takes a long time TPromise.timeout(instantProgress ? 0 : 800).then(() => { if (!resultPromiseDone && currentResultToken === this.currentResultToken) { - this.quickOpenWidget.getProgressBar().infinite().getContainer().show(); + this.quickOpenWidget.getProgressBar().infinite().show(); } }); @@ -793,7 +792,7 @@ export class QuickOpenController extends Component implements IQuickOpenService resultPromiseDone = true; if (currentResultToken === this.currentResultToken) { - this.quickOpenWidget.getProgressBar().getContainer().hide(); + this.quickOpenWidget.getProgressBar().hide(); } }, (error: any) => { resultPromiseDone = true; diff --git a/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts b/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts index c944aae93aa..d4826a2b8f6 100644 --- a/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts +++ b/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts @@ -25,8 +25,10 @@ import { Event } from 'vs/base/common/event'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { contrastBorder } from 'vs/platform/theme/common/colorRegistry'; import { SIDE_BAR_TITLE_FOREGROUND, SIDE_BAR_BACKGROUND, SIDE_BAR_FOREGROUND, SIDE_BAR_BORDER } from 'vs/workbench/common/theme'; -import { Dimension } from 'vs/base/browser/builder'; import { INotificationService } from 'vs/platform/notification/common/notification'; +import { Dimension, EventType } from 'vs/base/browser/dom'; +import { $ } from 'vs/base/browser/builder'; +import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; export class SidebarPart extends CompositePart { @@ -75,11 +77,18 @@ export class SidebarPart extends CompositePart { return this._onDidCompositeClose.event as Event; } + public createTitleArea(parent: HTMLElement): HTMLElement { + const titleArea = super.createTitleArea(parent); + $(titleArea).on(EventType.CONTEXT_MENU, (e: MouseEvent) => this.onTitleAreaContextMenu(new StandardMouseEvent(e))); + + return titleArea; + } + public updateStyles(): void { super.updateStyles(); // Part container - const container = this.getContainer(); + const container = $(this.getContainer()); container.style('background-color', this.getColor(SIDE_BAR_BACKGROUND)); container.style('color', this.getColor(SIDE_BAR_FOREGROUND)); @@ -132,6 +141,23 @@ export class SidebarPart extends CompositePart { return super.layout(dimension); } + + private onTitleAreaContextMenu(event: StandardMouseEvent): void { + const activeViewlet = this.getActiveViewlet() as Viewlet; + if (activeViewlet) { + const contextMenuActions = activeViewlet ? activeViewlet.getContextMenuActions() : []; + if (contextMenuActions.length) { + const anchor: { x: number, y: number } = { x: event.posx, y: event.posy }; + this.contextMenuService.showContextMenu({ + getAnchor: () => anchor, + getActions: () => TPromise.as(contextMenuActions), + getActionItem: action => this.actionItemProvider(action as Action), + actionRunner: activeViewlet.getActionRunner(), + getKeyBinding: action => this.keybindingService.lookupKeybinding(action.id) + }); + } + } + } } class FocusSideBarAction extends Action { diff --git a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts index 1004fac2c9b..73592ff0f2b 100644 --- a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts +++ b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts @@ -10,7 +10,7 @@ import * as nls from 'vs/nls'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { TPromise } from 'vs/base/common/winjs.base'; import { dispose, IDisposable } from 'vs/base/common/lifecycle'; -import { Builder, $ } from 'vs/base/browser/builder'; +import { $ } from 'vs/base/browser/builder'; import { OcticonLabel } from 'vs/base/browser/ui/octiconLabel/octiconLabel'; import { Registry } from 'vs/platform/registry/common/platform'; import { ICommandService } from 'vs/platform/commands/common/commands'; @@ -39,7 +39,7 @@ export class StatusbarPart extends Part implements IStatusbarService { private static readonly PRIORITY_PROP = 'priority'; private static readonly ALIGNMENT_PROP = 'alignment'; - private statusItemsContainer: Builder; + private statusItemsContainer: HTMLElement; private statusMsgDispose: IDisposable; private styleElement: HTMLStyleElement; @@ -67,7 +67,7 @@ export class StatusbarPart extends Part implements IStatusbarService { const toDispose = item.render(el); // Insert according to priority - const container = this.statusItemsContainer.getHTMLElement(); + const container = this.statusItemsContainer; const neighbours = this.getEntries(alignment); let inserted = false; for (let i = 0; i < neighbours.length; i++) { @@ -101,7 +101,7 @@ export class StatusbarPart extends Part implements IStatusbarService { private getEntries(alignment: StatusbarAlignment): HTMLElement[] { const entries: HTMLElement[] = []; - const container = this.statusItemsContainer.getHTMLElement(); + const container = this.statusItemsContainer; const children = container.children; for (let i = 0; i < children.length; i++) { const childElement = children.item(i); @@ -113,8 +113,8 @@ export class StatusbarPart extends Part implements IStatusbarService { return entries; } - public createContentArea(parent: Builder): Builder { - this.statusItemsContainer = $(parent); + public createContentArea(parent: HTMLElement): HTMLElement { + this.statusItemsContainer = parent; // Fill in initial items that were contributed from the registry const registry = Registry.as(Extensions.Statusbar); @@ -129,7 +129,7 @@ export class StatusbarPart extends Part implements IStatusbarService { const el = this.doCreateStatusItem(descriptor.alignment, descriptor.priority); const dispose = item.render(el); - this.statusItemsContainer.append(el); + this.statusItemsContainer.appendChild(el); return dispose; })); @@ -140,7 +140,7 @@ export class StatusbarPart extends Part implements IStatusbarService { protected updateStyles(): void { super.updateStyles(); - const container = this.getContainer(); + const container = $(this.getContainer()); // Background colors const backgroundColor = this.getColor(this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY ? STATUS_BAR_BACKGROUND : STATUS_BAR_NO_FOLDER_BACKGROUND); diff --git a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts index 49198a0da14..4fe96407a85 100644 --- a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts +++ b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts @@ -7,8 +7,7 @@ import 'vs/css!./media/titlebarpart'; import { TPromise } from 'vs/base/common/winjs.base'; -import { Builder, $, Dimension } from 'vs/base/browser/builder'; -import * as DOM from 'vs/base/browser/dom'; +import { Builder, $ } from 'vs/base/browser/builder'; import * as paths from 'vs/base/common/paths'; import { Part } from 'vs/workbench/browser/part'; import { ITitleService, ITitleProperties } from 'vs/workbench/services/title/common/titleService'; @@ -34,6 +33,7 @@ import { isMacintosh, isWindows } from 'vs/base/common/platform'; import URI from 'vs/base/common/uri'; import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { trim } from 'vs/base/common/strings'; +import { addDisposableListener, EventType, EventHelper, Dimension } from 'vs/base/browser/dom'; export class TitlebarPart extends Part implements ITitleService { @@ -86,8 +86,8 @@ export class TitlebarPart extends Part implements ITitleService { } private registerListeners(): void { - this.toUnbind.push(DOM.addDisposableListener(window, DOM.EventType.BLUR, () => this.onBlur())); - this.toUnbind.push(DOM.addDisposableListener(window, DOM.EventType.FOCUS, () => this.onFocus())); + this.toUnbind.push(addDisposableListener(window, EventType.BLUR, () => this.onBlur())); + this.toUnbind.push(addDisposableListener(window, EventType.FOCUS, () => this.onFocus())); this.toUnbind.push(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationChanged(e))); this.toUnbind.push(this.editorGroupService.onEditorsChanged(() => this.onEditorsChanged())); this.toUnbind.push(this.contextService.onDidChangeWorkspaceFolders(() => this.setTitle(this.getWindowTitle()))); @@ -226,7 +226,7 @@ export class TitlebarPart extends Part implements ITitleService { }); } - public createContentArea(parent: Builder): Builder { + public createContentArea(parent: HTMLElement): HTMLElement { this.titleContainer = $(parent); // Title @@ -236,16 +236,16 @@ export class TitlebarPart extends Part implements ITitleService { } // Maximize/Restore on doubleclick - this.titleContainer.on(DOM.EventType.DBLCLICK, (e) => { - DOM.EventHelper.stop(e); + this.titleContainer.on(EventType.DBLCLICK, (e) => { + EventHelper.stop(e); this.onTitleDoubleclick(); }); // Context menu on title - this.title.on([DOM.EventType.CONTEXT_MENU, DOM.EventType.MOUSE_DOWN], (e: MouseEvent) => { - if (e.type === DOM.EventType.CONTEXT_MENU || e.metaKey) { - DOM.EventHelper.stop(e); + this.title.on([EventType.CONTEXT_MENU, EventType.MOUSE_DOWN], (e: MouseEvent) => { + if (e.type === EventType.CONTEXT_MENU || e.metaKey) { + EventHelper.stop(e); this.onContextMenu(e); } @@ -253,7 +253,7 @@ export class TitlebarPart extends Part implements ITitleService { // Since the title area is used to drag the window, we do not want to steal focus from the // currently active element. So we restore focus after a timeout back to where it was. - this.titleContainer.on([DOM.EventType.MOUSE_DOWN], () => { + this.titleContainer.on([EventType.MOUSE_DOWN], () => { const active = document.activeElement; setTimeout(() => { if (active instanceof HTMLElement) { @@ -262,20 +262,19 @@ export class TitlebarPart extends Part implements ITitleService { }, 0 /* need a timeout because we are in capture phase */); }, void 0, true /* use capture to know the currently active element properly */); - return this.titleContainer; + return this.titleContainer.getHTMLElement(); } protected updateStyles(): void { super.updateStyles(); // Part container - const container = this.getContainer(); - if (container) { - container.style('color', this.getColor(this.isInactive ? TITLE_BAR_INACTIVE_FOREGROUND : TITLE_BAR_ACTIVE_FOREGROUND)); - container.style('background-color', this.getColor(this.isInactive ? TITLE_BAR_INACTIVE_BACKGROUND : TITLE_BAR_ACTIVE_BACKGROUND)); + if (this.titleContainer) { + this.titleContainer.style('color', this.getColor(this.isInactive ? TITLE_BAR_INACTIVE_FOREGROUND : TITLE_BAR_ACTIVE_FOREGROUND)); + this.titleContainer.style('background-color', this.getColor(this.isInactive ? TITLE_BAR_INACTIVE_BACKGROUND : TITLE_BAR_ACTIVE_BACKGROUND)); const titleBorder = this.getColor(TITLE_BAR_BORDER); - container.style('border-bottom', titleBorder ? `1px solid ${titleBorder}` : null); + this.titleContainer.style('border-bottom', titleBorder ? `1px solid ${titleBorder}` : null); } } diff --git a/src/vs/workbench/browser/parts/views/contributableViews.ts b/src/vs/workbench/browser/parts/views/contributableViews.ts new file mode 100644 index 00000000000..42c9ea86328 --- /dev/null +++ b/src/vs/workbench/browser/parts/views/contributableViews.ts @@ -0,0 +1,446 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { ViewsRegistry, IViewDescriptor, ViewLocation } from 'vs/workbench/common/views'; +import { IContextKeyService, IContextKeyChangeEvent, IReadableSet } from 'vs/platform/contextkey/common/contextkey'; +import { Event, chain, filterEvent, Emitter } from 'vs/base/common/event'; +import { sortedDiff, firstIndex, move } from 'vs/base/common/arrays'; +import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; +import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; + +function filterViewEvent(location: ViewLocation, event: Event): Event { + return chain(event) + .map(views => views.filter(view => view.location === location)) + .filter(views => views.length > 0) + .event; +} + +class CounterSet implements IReadableSet { + + private map = new Map(); + + add(value: T): CounterSet { + this.map.set(value, (this.map.get(value) || 0) + 1); + return this; + } + + delete(value: T): boolean { + let counter = this.map.get(value) || 0; + + if (counter === 0) { + return false; + } + + counter--; + + if (counter === 0) { + this.map.delete(value); + } else { + this.map.set(value, counter); + } + + return true; + } + + has(value: T): boolean { + return this.map.has(value); + } +} + +export interface IViewItem { + viewDescriptor: IViewDescriptor; + active: boolean; +} + +class ViewDescriptorCollection { + + private contextKeys = new CounterSet(); + private items: IViewItem[] = []; + private disposables: IDisposable[] = []; + + private _onDidChange = new Emitter(); + readonly onDidChange: Event = this._onDidChange.event; + + get viewDescriptors(): IViewDescriptor[] { + return this.items + .filter(i => i.active) + .map(i => i.viewDescriptor); + } + + constructor( + location: ViewLocation, + @IContextKeyService private contextKeyService: IContextKeyService + ) { + const onRelevantViewsRegistered = filterViewEvent(location, ViewsRegistry.onViewsRegistered); + onRelevantViewsRegistered(this.onViewsRegistered, this, this.disposables); + + const onRelevantViewsDeregistered = filterViewEvent(location, ViewsRegistry.onViewsDeregistered); + onRelevantViewsDeregistered(this.onViewsDeregistered, this, this.disposables); + + const onRelevantContextChange = filterEvent(contextKeyService.onDidChangeContext, e => e.affectsSome(this.contextKeys)); + onRelevantContextChange(this.onContextChanged, this); + + this.onViewsRegistered(ViewsRegistry.getViews(location)); + } + + private onViewsRegistered(viewDescriptors: IViewDescriptor[]): any { + let fireChangeEvent = false; + + for (const viewDescriptor of viewDescriptors) { + const item = { + viewDescriptor, + active: this.isViewDescriptorActive(viewDescriptor) // TODO: should read from some state? + }; + + this.items.push(item); + + if (viewDescriptor.when) { + for (const key of viewDescriptor.when.keys()) { + this.contextKeys.add(key); + } + } + + if (item.active) { + fireChangeEvent = true; + } + } + + if (fireChangeEvent) { + this._onDidChange.fire(); + } + } + + private onViewsDeregistered(viewDescriptors: IViewDescriptor[]): any { + let fireChangeEvent = false; + + for (const viewDescriptor of viewDescriptors) { + const index = firstIndex(this.items, i => i.viewDescriptor.id === viewDescriptor.id); + + if (index === -1) { + continue; + } + + const item = this.items[index]; + this.items.splice(index, 1); + + if (viewDescriptor.when) { + for (const key of viewDescriptor.when.keys()) { + this.contextKeys.delete(key); + } + } + + if (item.active) { + fireChangeEvent = true; + } + } + + if (fireChangeEvent) { + this._onDidChange.fire(); + } + } + + private onContextChanged(event: IContextKeyChangeEvent): any { + let fireChangeEvent = false; + + for (const item of this.items) { + const active = this.isViewDescriptorActive(item.viewDescriptor); + + if (item.active !== active) { + fireChangeEvent = true; + } + + item.active = active; + } + + if (fireChangeEvent) { + this._onDidChange.fire(); + } + } + + private isViewDescriptorActive(viewDescriptor: IViewDescriptor): boolean { + return !viewDescriptor.when || this.contextKeyService.contextMatchesRules(viewDescriptor.when); + } + + dispose(): void { + this.disposables = dispose(this.disposables); + } +} + +export interface IView { + viewDescriptor: IViewDescriptor; + visible: boolean; +} + +export interface IViewState { + visible: boolean; + collapsed: boolean; + order?: number; + size?: number; +} + +export interface IViewDescriptorRef { + viewDescriptor: IViewDescriptor; + index: number; +} + +export interface IAddedViewDescriptorRef extends IViewDescriptorRef { + collapsed: boolean; + size?: number; +} + +export class ContributableViewsModel { + + readonly viewDescriptors: IViewDescriptor[] = []; + get visibleViewDescriptors(): IViewDescriptor[] { + return this.viewDescriptors.filter(v => this.viewStates.get(v.id).visible); + } + + private _onDidAdd = new Emitter(); + readonly onDidAdd: Event = this._onDidAdd.event; + + private _onDidRemove = new Emitter(); + readonly onDidRemove: Event = this._onDidRemove.event; + + private _onDidMove = new Emitter<{ from: IViewDescriptorRef; to: IViewDescriptorRef; }>(); + readonly onDidMove: Event<{ from: IViewDescriptorRef; to: IViewDescriptorRef; }> = this._onDidMove.event; + + private disposables: IDisposable[] = []; + + constructor( + location: ViewLocation, + @IContextKeyService contextKeyService: IContextKeyService, + protected viewStates = new Map() + ) { + const viewDescriptorCollection = new ViewDescriptorCollection(location, contextKeyService); + this.disposables.push(viewDescriptorCollection); + + viewDescriptorCollection.onDidChange(() => this.onDidChangeViewDescriptors(viewDescriptorCollection.viewDescriptors), this, this.disposables); + this.onDidChangeViewDescriptors(viewDescriptorCollection.viewDescriptors); + } + + isVisible(id: string): boolean { + const state = this.viewStates.get(id); + + if (!state) { + throw new Error(`Unknown view ${id}`); + } + + return state.visible; + } + + setVisible(id: string, visible: boolean): void { + const { visibleIndex, viewDescriptor, state } = this.find(id); + + if (!viewDescriptor.canToggleVisibility) { + throw new Error(`Can't toggle this view's visibility`); + } + + if (state.visible === visible) { + return; + } + + state.visible = visible; + + if (visible) { + this._onDidAdd.fire({ index: visibleIndex, viewDescriptor, size: state.size, collapsed: state.collapsed }); + } else { + this._onDidRemove.fire({ index: visibleIndex, viewDescriptor }); + } + } + + isCollapsed(id: string): boolean { + const state = this.viewStates.get(id); + + if (!state) { + throw new Error(`Unknown view ${id}`); + } + + return state.collapsed; + } + + setCollapsed(id: string, collapsed: boolean): void { + const { state } = this.find(id); + state.collapsed = collapsed; + } + + getSize(id: string): number | undefined { + const state = this.viewStates.get(id); + + if (!state) { + throw new Error(`Unknown view ${id}`); + } + + return state.size; + } + + setSize(id: string, size: number): void { + const { state } = this.find(id); + state.size = size; + } + + move(from: string, to: string): void { + const fromIndex = firstIndex(this.viewDescriptors, v => v.id === from); + const toIndex = firstIndex(this.viewDescriptors, v => v.id === to); + + const fromViewDescriptor = this.viewDescriptors[fromIndex]; + const toViewDescriptor = this.viewDescriptors[toIndex]; + + move(this.viewDescriptors, fromIndex, toIndex); + + for (let index = 0; index < this.viewDescriptors.length; index++) { + const state = this.viewStates.get(this.viewDescriptors[index].id); + state.order = index; + } + + this._onDidMove.fire({ + from: { index: fromIndex, viewDescriptor: fromViewDescriptor }, + to: { index: toIndex, viewDescriptor: toViewDescriptor } + }); + } + + private find(id: string): { index: number, visibleIndex: number, viewDescriptor: IViewDescriptor, state: IViewState } { + for (let i = 0, visibleIndex = 0; i < this.viewDescriptors.length; i++) { + const viewDescriptor = this.viewDescriptors[i]; + const state = this.viewStates.get(viewDescriptor.id); + + if (viewDescriptor.id === id) { + return { index: i, visibleIndex, viewDescriptor, state }; + } + + if (state.visible) { + visibleIndex++; + } + } + + throw new Error(`view descriptor ${id} not found`); + } + + private compareViewDescriptors(a: IViewDescriptor, b: IViewDescriptor): number { + const viewStateA = this.viewStates.get(a.id); + const viewStateB = this.viewStates.get(b.id); + + let orderA = viewStateA && viewStateA.order; + orderA = typeof orderA === 'number' ? orderA : a.order; + orderA = typeof orderA === 'number' ? orderA : Number.POSITIVE_INFINITY; + + let orderB = viewStateB && viewStateB.order; + orderB = typeof orderB === 'number' ? orderB : b.order; + orderB = typeof orderB === 'number' ? orderB : Number.POSITIVE_INFINITY; + + if (orderA !== orderB) { + return orderA - orderB; + } + + if (a.id === b.id) { + return 0; + } + + return a.id < b.id ? -1 : 1; + } + + private onDidChangeViewDescriptors(viewDescriptors: IViewDescriptor[]): void { + const ids = new Set(); + + for (const viewDescriptor of this.viewDescriptors) { + ids.add(viewDescriptor.id); + } + + viewDescriptors = viewDescriptors.sort(this.compareViewDescriptors.bind(this)); + + for (const viewDescriptor of viewDescriptors) { + if (!this.viewStates.has(viewDescriptor.id)) { + this.viewStates.set(viewDescriptor.id, { + visible: true, + collapsed: false + }); + } + } + + const splices = sortedDiff( + this.viewDescriptors, + viewDescriptors, + this.compareViewDescriptors.bind(this) + ).reverse(); + + for (const splice of splices) { + const startViewDescriptor = this.viewDescriptors[splice.start]; + let startIndex = startViewDescriptor ? this.find(startViewDescriptor.id).visibleIndex : this.viewDescriptors.length; + + for (let i = 0; i < splice.deleteCount; i++) { + const viewDescriptor = this.viewDescriptors[splice.start + i]; + const { state } = this.find(viewDescriptor.id); + + if (state.visible) { + this._onDidRemove.fire({ index: startIndex, viewDescriptor }); + } + } + + for (let i = 0; i < splice.toInsert.length; i++) { + const viewDescriptor = splice.toInsert[i]; + const state = this.viewStates.get(viewDescriptor.id); + + if (state.visible) { + this._onDidAdd.fire({ index: startIndex++, viewDescriptor, size: state.size, collapsed: state.collapsed }); + } + } + } + + this.viewDescriptors.splice(0, this.viewDescriptors.length, ...viewDescriptors); + } + + dispose(): void { + this.disposables = dispose(this.disposables); + } +} + +interface ISerializedViewState { + id: string; + state: IViewState; +} + +export class PersistentContributableViewsModel extends ContributableViewsModel { + + private viewletStateStorageId: string; + private storageService: IStorageService; + private contextService: IWorkspaceContextService; + + constructor( + location: ViewLocation, + viewletStateStorageId: string, + @IContextKeyService contextKeyService: IContextKeyService, + @IStorageService storageService: IStorageService, + @IWorkspaceContextService contextService: IWorkspaceContextService + ) { + const scope = contextService.getWorkbenchState() !== WorkbenchState.EMPTY ? StorageScope.WORKSPACE : StorageScope.GLOBAL; + const raw = storageService.get(viewletStateStorageId, scope, '[]'); + const serializedViewsStates = JSON.parse(raw) as ISerializedViewState[]; + const viewStates = new Map(); + + for (const { id, state } of serializedViewsStates) { + viewStates.set(id, state); + } + + super(location, contextKeyService, viewStates); + + this.viewletStateStorageId = viewletStateStorageId; + this.storageService = storageService; + this.contextService = contextService; + } + + saveViewsStates(): void { + const serializedViewStates: ISerializedViewState[] = []; + this.viewStates.forEach((state, id) => serializedViewStates.push({ id, state })); + const raw = JSON.stringify(serializedViewStates); + + const scope = this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY ? StorageScope.WORKSPACE : StorageScope.GLOBAL; + this.storageService.store(this.viewletStateStorageId, raw, scope); + } + + dispose(): void { + this.saveViewsStates(); + super.dispose(); + } +} \ No newline at end of file diff --git a/src/vs/workbench/browser/parts/views/customView.ts b/src/vs/workbench/browser/parts/views/customView.ts index 66f4b10dd65..c064cc0bccd 100644 --- a/src/vs/workbench/browser/parts/views/customView.ts +++ b/src/vs/workbench/browser/parts/views/customView.ts @@ -9,7 +9,6 @@ import { IDisposable, Disposable, dispose } from 'vs/base/common/lifecycle'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { TPromise } from 'vs/base/common/winjs.base'; import * as DOM from 'vs/base/browser/dom'; -import { $ } from 'vs/base/browser/builder'; import { LIGHT, FileThemeIcon, FolderThemeIcon } from 'vs/platform/theme/common/themeService'; import { ITree, IDataSource, IRenderer, ContextMenuEvent } from 'vs/base/parts/tree/browser/tree'; import { TreeItemCollapsibleState, ITreeItem, ITreeViewer, ICustomViewsService, ITreeViewDataProvider, ViewsRegistry, IViewDescriptor, TreeViewItemHandleArg, ICustomViewDescriptor, IViewsViewlet } from 'vs/workbench/common/views'; @@ -171,9 +170,9 @@ class CustomTreeViewer extends Disposable implements ITreeViewer { if (this.tree) { if (this.isVisible) { - $(this.tree.getHTMLElement()).show(); + DOM.show(this.tree.getHTMLElement()); } else { - $(this.tree.getHTMLElement()).hide(); // make sure the tree goes out of the tabindex world by hiding it + DOM.hide(this.tree.getHTMLElement()); // make sure the tree goes out of the tabindex world by hiding it } if (this.isVisible) { @@ -265,7 +264,7 @@ class CustomTreeViewer extends Disposable implements ITreeViewer { return result.then(() => this.tree.reveal(item)) .then(() => { if (select) { - this.tree.setSelection([item]); + this.tree.setSelection([item], { source: 'api' }); } }); } @@ -287,6 +286,9 @@ class CustomTreeViewer extends Disposable implements ITreeViewer { } private onSelection({ payload }: any): void { + if (payload && payload.source === 'api') { + return; + } const selection: ITreeItem = this.tree.getSelection()[0]; if (selection) { if (selection.command) { diff --git a/src/vs/workbench/browser/parts/views/panelViewlet.ts b/src/vs/workbench/browser/parts/views/panelViewlet.ts index 97f55d94df5..76656e42490 100644 --- a/src/vs/workbench/browser/parts/views/panelViewlet.ts +++ b/src/vs/workbench/browser/parts/views/panelViewlet.ts @@ -10,8 +10,7 @@ import { Event, Emitter, filterEvent } from 'vs/base/common/event'; import { ColorIdentifier, contrastBorder } from 'vs/platform/theme/common/colorRegistry'; import { attachStyler, IColorMapping } from 'vs/platform/theme/common/styler'; import { SIDE_BAR_DRAG_AND_DROP_BACKGROUND, SIDE_BAR_SECTION_HEADER_FOREGROUND, SIDE_BAR_SECTION_HEADER_BACKGROUND } from 'vs/workbench/common/theme'; -import { Dimension, Builder } from 'vs/base/browser/builder'; -import { append, $, trackFocus, toggleClass, EventType, isAncestor } from 'vs/base/browser/dom'; +import { append, $, trackFocus, toggleClass, EventType, isAncestor, Dimension, addDisposableListener } from 'vs/base/browser/dom'; import { IDisposable, combinedDisposable } from 'vs/base/common/lifecycle'; import { firstIndex } from 'vs/base/common/arrays'; import { IAction, IActionRunner } from 'vs/base/common/actions'; @@ -151,6 +150,10 @@ export class PanelViewlet extends Viewlet { return this.panelview.onDidSashChange; } + protected get panels(): ViewletPanel[] { + return this.panelItems.map(i => i.panel); + } + protected get length(): number { return this.panelItems.length; } @@ -166,13 +169,12 @@ export class PanelViewlet extends Viewlet { super(id, partService, telemetryService, themeService); } - async create(parent: Builder): TPromise { + async create(parent: HTMLElement): TPromise { super.create(parent); - const container = parent.getHTMLElement(); - this.panelview = this._register(new PanelView(container, this.options)); + this.panelview = this._register(new PanelView(parent, this.options)); this._register(this.panelview.onDidDrop(({ from, to }) => this.movePanel(from as ViewletPanel, to as ViewletPanel))); - this._register(parent.on(EventType.CONTEXT_MENU, (e: MouseEvent) => this.showContextMenu(new StandardMouseEvent(e)))); + this._register(addDisposableListener(parent, EventType.CONTEXT_MENU, (e: MouseEvent) => this.showContextMenu(new StandardMouseEvent(e)))); } private showContextMenu(event: StandardMouseEvent): void { @@ -245,7 +247,20 @@ export class PanelViewlet extends Viewlet { return Math.max(...sizes); } - addPanel(panel: ViewletPanel, size: number, index = this.panelItems.length - 1): void { + addPanels(panels: { panel: ViewletPanel, size: number, index?: number }[]): void { + const wasSingleView = this.isSingleView(); + + for (const { panel, size, index } of panels) { + this.addPanel(panel, size, index); + } + + this.updateViewHeaders(); + if (this.isSingleView() !== wasSingleView) { + this.updateTitleArea(); + } + } + + private addPanel(panel: ViewletPanel, size: number, index = this.panelItems.length - 1): void { const disposables: IDisposable[] = []; const onDidFocus = panel.onDidFocus(() => this.lastFocusedPanel = panel, null, disposables); const onDidChange = panel.onDidChange(() => { @@ -263,17 +278,22 @@ export class PanelViewlet extends Viewlet { const disposable = combinedDisposable([onDidFocus, panelStyler, onDidChange]); const panelItem: IViewletPanelItem = { panel, disposable }; - const wasSingleView = this.isSingleView(); this.panelItems.splice(index, 0, panelItem); this.panelview.addPanel(panel, size, index); + } + + removePanels(panels: ViewletPanel[]): void { + const wasSingleView = this.isSingleView(); + + panels.forEach(panel => this.removePanel(panel)); this.updateViewHeaders(); - if (this.isSingleView() !== wasSingleView) { + if (wasSingleView !== this.isSingleView()) { this.updateTitleArea(); } } - removePanel(panel: ViewletPanel): void { + private removePanel(panel: ViewletPanel): void { const index = firstIndex(this.panelItems, i => i.panel === panel); if (index === -1) { @@ -284,15 +304,10 @@ export class PanelViewlet extends Viewlet { this.lastFocusedPanel = undefined; } - const wasSingleView = this.isSingleView(); this.panelview.removePanel(panel); const [panelItem] = this.panelItems.splice(index, 1); panelItem.disposable.dispose(); - this.updateViewHeaders(); - if (wasSingleView !== this.isSingleView()) { - this.updateTitleArea(); - } } movePanel(from: ViewletPanel, to: ViewletPanel): void { diff --git a/src/vs/workbench/browser/parts/views/viewsViewlet.ts b/src/vs/workbench/browser/parts/views/viewsViewlet.ts index cadcc3059eb..18f58d76c88 100644 --- a/src/vs/workbench/browser/parts/views/viewsViewlet.ts +++ b/src/vs/workbench/browser/parts/views/viewsViewlet.ts @@ -6,7 +6,6 @@ import { TPromise } from 'vs/base/common/winjs.base'; import * as errors from 'vs/base/common/errors'; import * as DOM from 'vs/base/browser/dom'; -import { $, Dimension, Builder } from 'vs/base/browser/builder'; import { Scope } from 'vs/workbench/common/memento'; import { dispose, IDisposable } from 'vs/base/common/lifecycle'; import { IAction, IActionRunner } from 'vs/base/common/actions'; @@ -24,7 +23,7 @@ import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/ import { IContextKeyService, IContextKeyChangeEvent } from 'vs/platform/contextkey/common/contextkey'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { PanelViewlet, ViewletPanel } from 'vs/workbench/browser/parts/views/panelViewlet'; -import { IPanelOptions } from 'vs/base/browser/ui/splitview/panelview'; +import { IPanelOptions, DefaultPanelDndController } from 'vs/base/browser/ui/splitview/panelview'; import { WorkbenchTree, IListService } from 'vs/platform/list/browser/listService'; import { IWorkbenchThemeService, IFileIconTheme } from 'vs/workbench/services/themes/common/workbenchThemeService'; import { ITreeConfiguration, ITreeOptions } from 'vs/base/parts/tree/browser/tree'; @@ -137,9 +136,9 @@ export abstract class TreeViewsViewletPanel extends ViewsViewletPanel { } if (isVisible) { - $(tree.getHTMLElement()).show(); + DOM.show(tree.getHTMLElement()); } else { - $(tree.getHTMLElement()).hide(); // make sure the tree goes out of the tabindex world by hiding it + DOM.hide(tree.getHTMLElement()); // make sure the tree goes out of the tabindex world by hiding it } if (isVisible) { @@ -190,7 +189,7 @@ export class ViewsViewlet extends PanelViewlet implements IViewsViewlet { private readonly viewsContextKeys: Set = new Set(); private viewsViewletPanels: ViewsViewletPanel[] = []; private didLayout = false; - private dimension: Dimension; + private dimension: DOM.Dimension; protected viewsStates: Map = new Map(); private areExtensionsReady: boolean = false; @@ -210,12 +209,12 @@ export class ViewsViewlet extends PanelViewlet implements IViewsViewlet { @IContextMenuService protected contextMenuService: IContextMenuService, @IExtensionService protected extensionService: IExtensionService ) { - super(id, { showHeaderInTitleWhenSingleView, dnd: true }, partService, contextMenuService, telemetryService, themeService); + super(id, { showHeaderInTitleWhenSingleView, dnd: new DefaultPanelDndController() }, partService, contextMenuService, telemetryService, themeService); this.viewletSettings = this.getMemento(storageService, Scope.WORKSPACE); } - async create(parent: Builder): TPromise { + async create(parent: HTMLElement): TPromise { await super.create(parent); this._register(this.onDidSashChange(() => this.snapshotViewsStates())); @@ -276,7 +275,7 @@ export class ViewsViewlet extends PanelViewlet implements IViewsViewlet { } } - layout(dimension: Dimension): void { + layout(dimension: DOM.Dimension): void { super.layout(dimension); this.dimension = dimension; if (this.didLayout) { @@ -371,14 +370,15 @@ export class ViewsViewlet extends PanelViewlet implements IViewsViewlet { this.snapshotViewsStates(); if (toRemove.length) { - for (const viewDescriptor of toRemove) { - let view = this.getView(viewDescriptor.id); - this.removePanel(view); - this.viewsViewletPanels.splice(this.viewsViewletPanels.indexOf(view), 1); - view.dispose(); + const panelsToRemove: ViewsViewletPanel[] = toRemove.map(viewDescriptor => this.getView(viewDescriptor.id)); + this.removePanels(panelsToRemove); + for (const panel of panelsToRemove) { + this.viewsViewletPanels.splice(this.viewsViewletPanels.indexOf(panel), 1); + panel.dispose(); } } + const panelsToAdd: { panel: ViewsViewletPanel, size: number, index: number }[] = []; for (const viewDescriptor of toAdd) { let viewState = this.viewsStates.get(viewDescriptor.id); let index = visible.indexOf(viewDescriptor); @@ -393,8 +393,13 @@ export class ViewsViewlet extends PanelViewlet implements IViewsViewlet { toCreate.push(view); const size = (viewState && viewState.size) || 200; - this.addPanel(view, size, index); - this.viewsViewletPanels.splice(index, 0, view); + panelsToAdd.push({ panel: view, size, index }); + } + + this.addPanels(panelsToAdd); + + for (const { panel, index } of panelsToAdd) { + this.viewsViewletPanels.splice(index, 0, panel); } return TPromise.join(toCreate.map(view => view.create())) @@ -545,19 +550,17 @@ export class ViewsViewlet extends PanelViewlet implements IViewsViewlet { return false; } - if (ViewLocation.getContributedViewLocation(this.location.id)) { - let visibleViewsCount = 0; - if (this.areExtensionsReady) { - visibleViewsCount = this.getViewDescriptorsFromRegistry().reduce((visibleViewsCount, v) => visibleViewsCount + (this.canBeVisible(v) ? 1 : 0), 0); - } else { + if (ViewLocation.get(this.location.id)) { + if (!this.areExtensionsReady) { + let visibleViewsCount = 0; // Check in cache so that view do not jump. See #29609 this.viewsStates.forEach((viewState, id) => { if (!viewState.isHidden) { visibleViewsCount++; } }); + return visibleViewsCount === 1; } - return visibleViewsCount === 1; } return super.isSingleView(); @@ -644,7 +647,7 @@ export class PersistentViewsViewlet extends ViewsViewlet { this._register(this.onDidChangeViewVisibilityState(id => this.onViewVisibilityChanged(id))); } - create(parent: Builder): TPromise { + create(parent: HTMLElement): TPromise { this.loadViewsStates(); return super.create(parent); } diff --git a/src/vs/workbench/browser/viewlet.ts b/src/vs/workbench/browser/viewlet.ts index 1d5419d0e5e..e6ed31632fe 100644 --- a/src/vs/workbench/browser/viewlet.ts +++ b/src/vs/workbench/browser/viewlet.ts @@ -154,7 +154,7 @@ export class ToggleViewletAction extends Action { const activeViewlet = this.viewletService.getActiveViewlet(); const activeElement = document.activeElement; - return activeViewlet && activeElement && DOM.isAncestor(activeElement, (activeViewlet).getContainer().getHTMLElement()); + return activeViewlet && activeElement && DOM.isAncestor(activeElement, (activeViewlet).getContainer()); } } diff --git a/src/vs/workbench/common/editor.ts b/src/vs/workbench/common/editor.ts index 12e0cbaae6b..29cb171b382 100644 --- a/src/vs/workbench/common/editor.ts +++ b/src/vs/workbench/common/editor.ts @@ -17,6 +17,7 @@ import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { Registry } from 'vs/platform/registry/common/platform'; import { ITextModel } from 'vs/editor/common/model'; import { Schemas } from 'vs/base/common/network'; +import { LRUCache } from 'vs/base/common/map'; export const TextCompareEditorVisible = new RawContextKey('textCompareEditorVisible', false); @@ -31,8 +32,6 @@ export enum ConfirmResult { */ export const TEXT_DIFF_EDITOR_ID = 'workbench.editors.textDiffEditor'; -export const PREFERENCES_EDITOR_ID = 'workbench.editor.preferencesEditor'; - /** * Binary diff editor id. */ @@ -908,6 +907,114 @@ export function toResource(editor: IEditorInput, options?: IResourceOptions): UR return null; } +export interface IEditorViewStates { + [Position.ONE]?: T; + [Position.TWO]?: T; + [Position.THREE]?: T; +} + +export class EditorViewStateMemento { + private cache: LRUCache>; + + constructor(private memento: object, private key: string, private limit: number = 10) { } + + public saveState(resource: URI, position: Position, state: T): void; + public saveState(editor: EditorInput, position: Position, state: T): void; + public saveState(resourceOrEditor: URI | EditorInput, position: Position, state: T): void { + if (typeof position !== 'number') { + return; // we need a position at least + } + + const resource = this.doGetResource(resourceOrEditor); + if (resource) { + const cache = this.doLoad(); + + let viewStates = cache.get(resource.toString()); + if (!viewStates) { + viewStates = Object.create(null) as IEditorViewStates; + cache.set(resource.toString(), viewStates); + } + + viewStates[position] = state; + + // Automatically clear when editor input gets disposed if any + if (resourceOrEditor instanceof EditorInput) { + once(resourceOrEditor.onDispose)(() => { + this.clearState(resource); + }); + } + } + } + + public loadState(resource: URI, position: Position): T; + public loadState(editor: EditorInput, position: Position): T; + public loadState(resourceOrEditor: URI | EditorInput, position: Position): T { + if (typeof position !== 'number') { + return void 0; // we need a position at least + } + + const resource = this.doGetResource(resourceOrEditor); + if (resource) { + const cache = this.doLoad(); + + const viewStates = cache.get(resource.toString()); + if (viewStates) { + return viewStates[position]; + } + } + + return void 0; + } + + public clearState(resource: URI): void; + public clearState(editor: EditorInput): void; + public clearState(resourceOrEditor: URI | EditorInput): void { + const resource = this.doGetResource(resourceOrEditor); + if (resource) { + const cache = this.doLoad(); + cache.delete(resource.toString()); + } + } + + private doGetResource(resourceOrEditor: URI | EditorInput): URI { + if (resourceOrEditor instanceof EditorInput) { + return resourceOrEditor.getResource(); + } + + return resourceOrEditor; + } + + private doLoad(): LRUCache> { + if (!this.cache) { + this.cache = new LRUCache(this.limit); + + // Restore from serialized map state + const rawViewState = this.memento[this.key]; + if (Array.isArray(rawViewState)) { + this.cache.fromJSON(rawViewState); + } + + // Migration from old object state + else if (rawViewState) { + const keys = Object.keys(rawViewState); + keys.forEach((key, index) => { + if (index < this.limit) { + this.cache.set(key, rawViewState[key]); + } + }); + } + } + + return this.cache; + } + + public save(): void { + const cache = this.doLoad(); + + this.memento[this.key] = cache.toJSON(); + } +} + class EditorInputFactoryRegistry implements IEditorInputFactoryRegistry { private instantiationService: IInstantiationService; private fileInputFactory: IFileInputFactory; diff --git a/src/vs/workbench/common/notifications.ts b/src/vs/workbench/common/notifications.ts index ebcfc0a44e3..d27b8209c2b 100644 --- a/src/vs/workbench/common/notifications.ts +++ b/src/vs/workbench/common/notifications.ts @@ -44,21 +44,21 @@ export interface INotificationChangeEvent { } export class NotificationHandle implements INotificationHandle { - private readonly _onDidDispose: Emitter = new Emitter(); + private readonly _onDidClose: Emitter = new Emitter(); - constructor(private item: INotificationViewItem, private disposeItem: (item: INotificationViewItem) => void) { + constructor(private item: INotificationViewItem, private closeItem: (item: INotificationViewItem) => void) { this.registerListeners(); } private registerListeners(): void { - once(this.item.onDidDispose)(() => { - this._onDidDispose.fire(); - this._onDidDispose.dispose(); + once(this.item.onDidClose)(() => { + this._onDidClose.fire(); + this._onDidClose.dispose(); }); } - public get onDidDispose(): Event { - return this._onDidDispose.event; + public get onDidClose(): Event { + return this._onDidClose.event; } public get progress(): INotificationProgress { @@ -77,9 +77,9 @@ export class NotificationHandle implements INotificationHandle { this.item.updateActions(actions); } - public dispose(): void { - this.disposeItem(this.item); - this._onDidDispose.dispose(); + public close(): void { + this.closeItem(this.item); + this._onDidClose.dispose(); } } @@ -117,7 +117,7 @@ export class NotificationsModel implements INotificationsModel { // Deduplicate const duplicate = this.findNotification(item); if (duplicate) { - duplicate.dispose(); + duplicate.close(); } // Add to list as first entry @@ -127,15 +127,15 @@ export class NotificationsModel implements INotificationsModel { this._onDidNotificationChange.fire({ item, index: 0, kind: NotificationChangeType.ADD }); // Wrap into handle - return new NotificationHandle(item, item => this.disposeItem(item)); + return new NotificationHandle(item, item => this.closeItem(item)); } - private disposeItem(item: INotificationViewItem): void { + private closeItem(item: INotificationViewItem): void { const liveItem = this.findNotification(item); if (liveItem && liveItem !== item) { - liveItem.dispose(); // item could have been replaced with another one, make sure to dispose the live item + liveItem.close(); // item could have been replaced with another one, make sure to close the live item } else { - item.dispose(); // otherwise just dispose the item that was passed in + item.close(); // otherwise just close the item that was passed in } } @@ -174,7 +174,7 @@ export class NotificationsModel implements INotificationsModel { } }); - once(item.onDidDispose)(() => { + once(item.onDidClose)(() => { itemExpansionChangeListener.dispose(); itemLabelChangeListener.dispose(); @@ -204,7 +204,7 @@ export interface INotificationViewItem { readonly canCollapse: boolean; readonly onDidExpansionChange: Event; - readonly onDidDispose: Event; + readonly onDidClose: Event; readonly onDidLabelChange: Event; expand(): void; @@ -217,7 +217,7 @@ export interface INotificationViewItem { updateMessage(message: NotificationMessage): void; updateActions(actions?: INotificationActions): void; - dispose(): void; + close(): void; equals(item: INotificationViewItem); } @@ -359,7 +359,7 @@ export class NotificationViewItem implements INotificationViewItem { private _progress: NotificationViewItemProgress; private readonly _onDidExpansionChange: Emitter; - private readonly _onDidDispose: Emitter; + private readonly _onDidClose: Emitter; private readonly _onDidLabelChange: Emitter; public static create(notification: INotification): INotificationViewItem { @@ -435,8 +435,8 @@ export class NotificationViewItem implements INotificationViewItem { this._onDidLabelChange = new Emitter(); this.toDispose.push(this._onDidLabelChange); - this._onDidDispose = new Emitter(); - this.toDispose.push(this._onDidDispose); + this._onDidClose = new Emitter(); + this.toDispose.push(this._onDidClose); } private setActions(actions: INotificationActions): void { @@ -464,8 +464,8 @@ export class NotificationViewItem implements INotificationViewItem { return this._onDidLabelChange.event; } - public get onDidDispose(): Event { - return this._onDidDispose.event; + public get onDidClose(): Event { + return this._onDidClose.event; } public get canCollapse(): boolean { @@ -556,13 +556,17 @@ export class NotificationViewItem implements INotificationViewItem { } } - public dispose(): void { - this._onDidDispose.fire(); + public close(): void { + this._onDidClose.fire(); this.toDispose = dispose(this.toDispose); } public equals(other: INotificationViewItem): boolean { + if (this.hasProgress() || other.hasProgress()) { + return false; + } + if (this._source !== other.source) { return false; } @@ -578,7 +582,7 @@ export class NotificationViewItem implements INotificationViewItem { } for (let i = 0; i < primaryActions.length; i++) { - if (primaryActions[i].id !== otherPrimaryActions[i].id) { + if ((primaryActions[i].id + primaryActions[i].label) !== (otherPrimaryActions[i].id + otherPrimaryActions[i].label)) { return false; } } diff --git a/src/vs/workbench/common/theme.ts b/src/vs/workbench/common/theme.ts index fc93969262c..13e52316a1c 100644 --- a/src/vs/workbench/common/theme.ts +++ b/src/vs/workbench/common/theme.ts @@ -361,7 +361,7 @@ export const TITLE_BAR_INACTIVE_BACKGROUND = registerColor('titleBar.inactiveBac export const TITLE_BAR_BORDER = registerColor('titleBar.border', { dark: null, light: null, - hc: null + hc: contrastBorder }, nls.localize('titleBarBorder', "Title bar border color. Note that this color is currently only supported on macOS.")); // < --- Notifications --- > diff --git a/src/vs/workbench/common/views.ts b/src/vs/workbench/common/views.ts index 5947ecba8cd..c72806df4fb 100644 --- a/src/vs/workbench/common/views.ts +++ b/src/vs/workbench/common/views.ts @@ -17,24 +17,24 @@ import { ThemeIcon } from 'vs/platform/theme/common/themeService'; export class ViewLocation { - static readonly Explorer = new ViewLocation('workbench.view.explorer'); - static readonly Debug = new ViewLocation('workbench.view.debug'); - static readonly Extensions = new ViewLocation('workbench.view.extensions'); - - constructor(private _id: string) { + private static locations: Map = new Map(); + static register(id: string): ViewLocation { + const viewLocation = new ViewLocation(id); + ViewLocation.locations.set(id, viewLocation); + return viewLocation; + } + static get(value: string): ViewLocation { + return ViewLocation.locations.get(value); } - get id(): string { - return this._id; - } + static readonly Explorer: ViewLocation = ViewLocation.register('workbench.view.explorer'); + static readonly Debug: ViewLocation = ViewLocation.register('workbench.view.debug'); + static readonly Extensions: ViewLocation = ViewLocation.register('workbench.view.extensions'); + static readonly SCM: ViewLocation = ViewLocation.register('workbench.view.scm.views.contributed'); + + private constructor(private _id: string) { } + get id(): string { return this._id; } - static getContributedViewLocation(value: string): ViewLocation { - switch (value) { - case 'explorer': return ViewLocation.Explorer; - case 'debug': return ViewLocation.Debug; - } - return void 0; - } } export interface IViewDescriptor { diff --git a/src/vs/workbench/electron-browser/actions.ts b/src/vs/workbench/electron-browser/actions.ts index 3e0b42414db..7abd5ddb6b1 100644 --- a/src/vs/workbench/electron-browser/actions.ts +++ b/src/vs/workbench/electron-browser/actions.ts @@ -871,6 +871,24 @@ export class OpenIssueReporterAction extends Action { } } +export class OpenProcessExplorer extends Action { + public static readonly ID = 'workbench.action.openProcessExplorer'; + public static readonly LABEL = nls.localize('openProcessExplorer', "Open Process Explorer"); + + constructor( + id: string, + label: string, + @IWorkbenchIssueService private issueService: IWorkbenchIssueService + ) { + super(id, label); + } + + public run(): TPromise { + return this.issueService.openProcessExplorer() + .then(() => true); + } +} + export class ReportPerformanceIssueUsingReporterAction extends Action { public static readonly ID = 'workbench.action.reportPerformanceIssueUsingReporter'; public static readonly LABEL = nls.localize('reportPerformanceIssue', "Report Performance Issue"); diff --git a/src/vs/workbench/electron-browser/bootstrap/index.js b/src/vs/workbench/electron-browser/bootstrap/index.js index 0c0b9b7c018..75f6dc4d7f6 100644 --- a/src/vs/workbench/electron-browser/bootstrap/index.js +++ b/src/vs/workbench/electron-browser/bootstrap/index.js @@ -28,7 +28,7 @@ process.lazyEnv = new Promise(function (resolve) { assign(process.env, shellEnv); resolve(process.env); }); - ipc.send('vscode:fetchShellEnv', remote.getCurrentWindow().id); + ipc.send('vscode:fetchShellEnv'); }); Error.stackTraceLimit = 100; // increase number of stack frames (from 10, https://github.com/v8/v8/wiki/Stack-Trace-API) diff --git a/src/vs/workbench/electron-browser/main.contribution.ts b/src/vs/workbench/electron-browser/main.contribution.ts index 87d47096eef..eb578183a03 100644 --- a/src/vs/workbench/electron-browser/main.contribution.ts +++ b/src/vs/workbench/electron-browser/main.contribution.ts @@ -9,17 +9,19 @@ import { Registry } from 'vs/platform/registry/common/platform'; import * as nls from 'vs/nls'; import product from 'vs/platform/node/product'; import * as os from 'os'; -import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; -import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry'; +import { SyncActionDescriptor, MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; +import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; import { IWorkbenchActionRegistry, Extensions } from 'vs/workbench/common/actions'; import { KeyMod, KeyChord, KeyCode } from 'vs/base/common/keyCodes'; import { isWindows, isLinux, isMacintosh } from 'vs/base/common/platform'; -import { KeybindingsReferenceAction, OpenDocumentationUrlAction, OpenIntroductoryVideosUrlAction, OpenTipsAndTricksUrlAction, OpenIssueReporterAction, ReportPerformanceIssueUsingReporterAction, ZoomResetAction, ZoomOutAction, ZoomInAction, ToggleFullScreenAction, ToggleMenuBarAction, CloseWorkspaceAction, CloseCurrentWindowAction, SwitchWindow, NewWindowAction, NavigateUpAction, NavigateDownAction, NavigateLeftAction, NavigateRightAction, IncreaseViewSizeAction, DecreaseViewSizeAction, ShowStartupPerformance, ToggleSharedProcessAction, QuickSwitchWindow, QuickOpenRecentAction, inRecentFilesPickerContextKey, ShowAboutDialogAction, InspectContextKeysAction } from 'vs/workbench/electron-browser/actions'; +import { KeybindingsReferenceAction, OpenDocumentationUrlAction, OpenIntroductoryVideosUrlAction, OpenTipsAndTricksUrlAction, OpenIssueReporterAction, ReportPerformanceIssueUsingReporterAction, ZoomResetAction, ZoomOutAction, ZoomInAction, ToggleFullScreenAction, ToggleMenuBarAction, CloseWorkspaceAction, CloseCurrentWindowAction, SwitchWindow, NewWindowAction, NavigateUpAction, NavigateDownAction, NavigateLeftAction, NavigateRightAction, IncreaseViewSizeAction, DecreaseViewSizeAction, ShowStartupPerformance, ToggleSharedProcessAction, QuickSwitchWindow, QuickOpenRecentAction, inRecentFilesPickerContextKey, ShowAboutDialogAction, InspectContextKeysAction, OpenProcessExplorer } from 'vs/workbench/electron-browser/actions'; import { registerCommands } from 'vs/workbench/electron-browser/commands'; import { AddRootFolderAction, GlobalRemoveRootFolderAction, OpenWorkspaceAction, SaveWorkspaceAsAction, OpenWorkspaceConfigFileAction, OpenFolderAsWorkspaceInNewWindowAction, OpenFileFolderAction, OpenFileAction, OpenFolderAction } from 'vs/workbench/browser/actions/workspaceActions'; -import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyExpr, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { inQuickOpenContext, getQuickNavigateHandler } from 'vs/workbench/browser/parts/quickopen/quickopen'; import { KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; // Contribute Commands registerCommands(); @@ -103,14 +105,25 @@ workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(AddRoo workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(GlobalRemoveRootFolderAction, GlobalRemoveRootFolderAction.ID, GlobalRemoveRootFolderAction.LABEL), 'Workspaces: Remove Folder from Workspace...', workspacesCategory); workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(OpenWorkspaceAction, OpenWorkspaceAction.ID, OpenWorkspaceAction.LABEL), 'Workspaces: Open Workspace...', workspacesCategory); workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(SaveWorkspaceAsAction, SaveWorkspaceAsAction.ID, SaveWorkspaceAsAction.LABEL), 'Workspaces: Save Workspace As...', workspacesCategory); -workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(OpenWorkspaceConfigFileAction, OpenWorkspaceConfigFileAction.ID, OpenWorkspaceConfigFileAction.LABEL), 'Workspaces: Open Workspace Configuration File', workspacesCategory); workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(OpenFolderAsWorkspaceInNewWindowAction, OpenFolderAsWorkspaceInNewWindowAction.ID, OpenFolderAsWorkspaceInNewWindowAction.LABEL), 'Workspaces: Open Folder as Workspace in New Window', workspacesCategory); +CommandsRegistry.registerCommand(OpenWorkspaceConfigFileAction.ID, serviceAccessor => { + serviceAccessor.get(IInstantiationService).createInstance(OpenWorkspaceConfigFileAction, OpenWorkspaceConfigFileAction.ID, OpenWorkspaceConfigFileAction.LABEL).run(); +}); +MenuRegistry.appendMenuItem(MenuId.CommandPalette, { + command: { + id: OpenWorkspaceConfigFileAction.ID, + title: `${workspacesCategory}: ${OpenWorkspaceConfigFileAction.LABEL}`, + }, + when: new RawContextKey('workbenchState', '').isEqualTo('workspace') +}); + // Developer related actions const developerCategory = nls.localize('developer', "Developer"); workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(ShowStartupPerformance, ShowStartupPerformance.ID, ShowStartupPerformance.LABEL), 'Developer: Startup Performance', developerCategory); workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(ToggleSharedProcessAction, ToggleSharedProcessAction.ID, ToggleSharedProcessAction.LABEL), 'Developer: Toggle Shared Process', developerCategory); workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(InspectContextKeysAction, InspectContextKeysAction.ID, InspectContextKeysAction.LABEL), 'Developer: Inspect Context Keys', developerCategory); +workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(OpenProcessExplorer, OpenProcessExplorer.ID, OpenProcessExplorer.LABEL), 'Developer: Open Process Explorer', developerCategory); const recentFilesPickerContext = ContextKeyExpr.and(inQuickOpenContext, ContextKeyExpr.has(inRecentFilesPickerContextKey)); @@ -298,6 +311,7 @@ configurationRegistry.registerConfiguration({ nls.localize('window.openFilesInNewWindow.default', "Files will open in a new window unless picked from within the application (e.g. via the File menu)") ], 'default': 'off', + 'scope': ConfigurationScope.APPLICATION, 'description': isMacintosh ? nls.localize('openFilesInNewWindowMac', "Controls if files should open in a new window.\n- default: files will open in the window with the files' folder open or the last active window unless opened via the Dock or from Finder\n- on: files will open in a new window\n- off: files will open in the window with the files' folder open or the last active window\nNote that there can still be cases where this setting is ignored (e.g. when using the -new-window or -reuse-window command line option).") : @@ -312,6 +326,7 @@ configurationRegistry.registerConfiguration({ nls.localize('window.openFoldersInNewWindow.default', "Folders will open in a new window unless a folder is picked from within the application (e.g. via the File menu)") ], 'default': 'default', + 'scope': ConfigurationScope.APPLICATION, 'description': nls.localize('openFoldersInNewWindow', "Controls if folders should open in a new window or replace the last active window.\n- default: folders will open in a new window unless a folder is picked from within the application (e.g. via the File menu)\n- on: folders will open in a new window\n- off: folders will replace the last active window\nNote that there can still be cases where this setting is ignored (e.g. when using the -new-window or -reuse-window command line option).") }, 'window.openWithoutArgumentsInNewWindow': { @@ -322,6 +337,7 @@ configurationRegistry.registerConfiguration({ nls.localize('window.openWithoutArgumentsInNewWindow.off', "Focus the last active running instance") ], 'default': isMacintosh ? 'off' : 'on', + 'scope': ConfigurationScope.APPLICATION, 'description': nls.localize('openWithoutArgumentsInNewWindow', "Controls if a new empty window should open when starting a second instance without arguments or if the last running instance should get focus.\n- on: open a new empty window\n- off: the last active running instance will get focus\nNote that there can still be cases where this setting is ignored (e.g. when using the -new-window or -reuse-window command line option).") }, 'window.restoreWindows': { @@ -334,11 +350,13 @@ configurationRegistry.registerConfiguration({ nls.localize('window.reopenFolders.none', "Never reopen a window. Always start with an empty one.") ], 'default': 'one', + 'scope': ConfigurationScope.APPLICATION, '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', 'default': false, + 'scope': ConfigurationScope.APPLICATION, 'description': nls.localize('restoreFullscreen', "Controls if a window should restore to full screen mode if it was exited in full screen mode.") }, 'window.zoomLevel': { @@ -362,6 +380,7 @@ configurationRegistry.registerConfiguration({ nls.localize('window.newWindowDimensions.fullscreen', "Open new windows in full screen mode.") ], 'default': 'default', + 'scope': ConfigurationScope.APPLICATION, 'description': nls.localize('newWindowDimensions', "Controls the dimensions of opening a new window when at least one window is already opened. By default, a new window will open in the center of the screen with small dimensions. When set to 'inherit', the window will get the same dimensions as the last window that was active. When set to 'maximized', the window will open maximized and fullscreen if configured to 'fullscreen'. Note that this setting does not have an impact on the first window that is opened. The first window will always restore the size and location as you left it before closing.") }, 'window.closeWhenEmpty': { @@ -379,12 +398,14 @@ configurationRegistry.registerConfiguration({ nls.localize('window.menuBarVisibility.hidden', "Menu is always hidden.") ], 'default': 'default', + 'scope': ConfigurationScope.APPLICATION, 'description': nls.localize('menuBarVisibility', "Control the visibility of the menu bar. A setting of 'toggle' means that the menu bar is hidden and a single press of the Alt key will show it. By default, the menu bar will be visible, unless the window is full screen."), 'included': isWindows || isLinux }, 'window.enableMenuBarMnemonics': { 'type': 'boolean', 'default': true, + 'scope': ConfigurationScope.APPLICATION, 'description': nls.localize('enableMenuBarMnemonics', "If enabled, the main menus can be opened via Alt-key shortcuts. Disabling mnemonics allows to bind these Alt-key shortcuts to editor commands instead."), 'included': isWindows || isLinux }, @@ -398,20 +419,30 @@ configurationRegistry.registerConfiguration({ 'type': 'string', 'enum': ['native', 'custom'], 'default': 'custom', + 'scope': ConfigurationScope.APPLICATION, 'description': nls.localize('titleBarStyle', "Adjust the appearance of the window title bar. Changes require a full restart to apply."), 'included': isMacintosh }, 'window.nativeTabs': { 'type': 'boolean', 'default': false, + 'scope': ConfigurationScope.APPLICATION, 'description': nls.localize('window.nativeTabs', "Enables macOS Sierra window tabs. Note that changes require a full restart to apply and that native tabs will disable a custom title bar style if configured."), 'included': isMacintosh && parseFloat(os.release()) >= 16 // Minimum: macOS Sierra (10.12.x = darwin 16.x) }, 'window.smoothScrollingWorkaround': { 'type': 'boolean', 'default': false, + 'scope': ConfigurationScope.APPLICATION, 'description': nls.localize('window.smoothScrollingWorkaround', "Enable this workaround if scrolling is no longer smooth after restoring a minimized VS Code window. This is a workaround for an issue (https://github.com/Microsoft/vscode/issues/13612) where scrolling starts to lag on devices with precision trackpads like the Surface devices from Microsoft. Enabling this workaround can result in a little bit of layout flickering after restoring the window from minimized state but is otherwise harmless."), 'included': isWindows + }, + 'window.clickThroughInactive': { + 'type': 'boolean', + 'default': true, + 'scope': ConfigurationScope.APPLICATION, + 'description': nls.localize('window.clickThroughInactive', "If enabled, clicking on an inactive window will both activate the window and trigger the element under the mouse if it is clickable. If disabled, clicking anywhere on an inactive window will activate it only and a second click is required on the element."), + 'included': isMacintosh } } }); diff --git a/src/vs/workbench/electron-browser/main.ts b/src/vs/workbench/electron-browser/main.ts index b5d1817b211..087bbe40d24 100644 --- a/src/vs/workbench/electron-browser/main.ts +++ b/src/vs/workbench/electron-browser/main.ts @@ -97,7 +97,7 @@ function openWorkbench(configuration: IWindowConfiguration): TPromise { logService, timerService, storageService - }, mainServices, configuration); + }, mainServices, mainProcessClient, configuration); shell.open(); // Inform user about loading issues from the loader diff --git a/src/vs/workbench/electron-browser/shell.ts b/src/vs/workbench/electron-browser/shell.ts index 21252971525..93ba3e987c8 100644 --- a/src/vs/workbench/electron-browser/shell.ts +++ b/src/vs/workbench/electron-browser/shell.ts @@ -9,8 +9,6 @@ import 'vs/css!./media/shell'; import * as platform from 'vs/base/common/platform'; import * as perf from 'vs/base/common/performance'; -import { Dimension, Builder, $ } from 'vs/base/browser/builder'; -import * as dom from 'vs/base/browser/dom'; import * as aria from 'vs/base/browser/ui/aria/aria'; import { dispose, IDisposable } from 'vs/base/common/lifecycle'; import * as errors from 'vs/base/common/errors'; @@ -62,7 +60,7 @@ import { WorkbenchModeServiceImpl } from 'vs/workbench/services/mode/common/work import { IModeService } from 'vs/editor/common/services/modeService'; import { IUntitledEditorService, UntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; import { ICrashReporterService, NullCrashReporterService, CrashReporterService } from 'vs/workbench/services/crashReporter/electron-browser/crashReporterService'; -import { getDelayedChannel } from 'vs/base/parts/ipc/common/ipc'; +import { getDelayedChannel, IPCClient } from 'vs/base/parts/ipc/common/ipc'; import { connect as connectNet } from 'vs/base/parts/ipc/node/ipc.net'; import { IExtensionManagementChannel, ExtensionManagementChannelClient } from 'vs/platform/extensionManagement/common/extensionManagementIpc'; import { IExtensionManagementService, IExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionManagement'; @@ -96,6 +94,7 @@ import { NotificationService } from 'vs/workbench/services/notification/common/n import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { DialogService } from 'vs/workbench/services/dialogs/electron-browser/dialogService'; import { DialogChannel } from 'vs/platform/dialogs/common/dialogIpc'; +import { EventType, addDisposableListener, addClass, getClientArea } from 'vs/base/browser/dom'; /** * Services that we require for the Shell @@ -135,12 +134,12 @@ export class WorkbenchShell { private previousErrorValue: string; private previousErrorTime: number; private content: HTMLElement; - private contentsContainer: Builder; + private contentsContainer: HTMLElement; private configuration: IWindowConfiguration; private workbench: Workbench; - constructor(container: HTMLElement, coreServices: ICoreServices, mainProcessServices: ServiceCollection, configuration: IWindowConfiguration) { + constructor(container: HTMLElement, coreServices: ICoreServices, mainProcessServices: ServiceCollection, private mainProcessClient: IPCClient, configuration: IWindowConfiguration) { this.container = container; this.configuration = configuration; @@ -158,19 +157,20 @@ export class WorkbenchShell { this.previousErrorTime = 0; } - private createContents(parent: Builder): Builder { + private createContents(parent: HTMLElement): HTMLElement { // ARIA aria.setARIAContainer(document.body); // Workbench Container - const workbenchContainer = $(parent).div(); + const workbenchContainer = document.createElement('div'); + parent.appendChild(workbenchContainer); // Instantiation service with services - const [instantiationService, serviceCollection] = this.initServiceCollection(parent.getHTMLElement()); + const [instantiationService, serviceCollection] = this.initServiceCollection(parent); // Workbench - this.workbench = this.createWorkbench(instantiationService, serviceCollection, parent.getHTMLElement(), workbenchContainer.getHTMLElement()); + this.workbench = this.createWorkbench(instantiationService, serviceCollection, parent, workbenchContainer); // Window this.workbench.getInstantiationService().createInstance(ElectronWindow, this.container); @@ -189,7 +189,7 @@ export class WorkbenchShell { private createWorkbench(instantiationService: IInstantiationService, serviceCollection: ServiceCollection, parent: HTMLElement, workbenchContainer: HTMLElement): Workbench { try { - const workbench = instantiationService.createInstance(Workbench, parent, workbenchContainer, this.configuration, serviceCollection, this.lifecycleService); + const workbench = instantiationService.createInstance(Workbench, parent, workbenchContainer, this.configuration, serviceCollection, this.lifecycleService, this.mainProcessClient); // Set lifecycle phase to `Restoring` this.lifecycleService.phase = LifecyclePhase.Restoring; @@ -312,7 +312,7 @@ export class WorkbenchShell { perf.mark('didStatLocalStorage'); /* __GDPR__ - "localStorageTimers2" : { + "localStorageTimers" : { "statTime" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "accessTime" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "firstReadTime" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, @@ -475,13 +475,15 @@ export class WorkbenchShell { }); // Shell Class for CSS Scoping - $(this.container).addClass('monaco-shell'); + addClass(this.container, 'monaco-shell'); // Controls - this.content = $('.monaco-shell-content').appendTo(this.container).getHTMLElement(); + this.content = document.createElement('div'); + addClass(this.content, 'monaco-shell-content'); + this.container.appendChild(this.content); // Create Contents - this.contentsContainer = this.createContents($(this.content)); + this.contentsContainer = this.createContents(this.content); // Layout this.layout(); @@ -493,7 +495,7 @@ export class WorkbenchShell { private registerListeners(): void { // Resize - $(window).on(dom.EventType.RESIZE, () => this.layout(), this.toUnbind); + this.toUnbind.push(addDisposableListener(window, EventType.RESIZE, () => this.layout())); } public onUnexpectedError(error: any): void { @@ -520,10 +522,10 @@ export class WorkbenchShell { } private layout(): void { - const clArea = $(this.container).getClientArea(); + const clientArea = getClientArea(this.container); - const contentsSize = new Dimension(clArea.width, clArea.height); - this.contentsContainer.size(contentsSize.width, contentsSize.height); + this.contentsContainer.style.width = `${clientArea.width}px`; + this.contentsContainer.style.height = `${clientArea.height}px`; this.contextViewService.layout(); this.workbench.layout(); diff --git a/src/vs/workbench/electron-browser/window.ts b/src/vs/workbench/electron-browser/window.ts index 53402073fbf..5da959f79eb 100644 --- a/src/vs/workbench/electron-browser/window.ts +++ b/src/vs/workbench/electron-browser/window.ts @@ -131,7 +131,7 @@ export class ElectronWindow extends Themable { }); // Support runAction event - ipc.on('vscode:runAction', (_event: any, request: IRunActionInWindowRequest) => { + ipc.on('vscode:runAction', (event: any, request: IRunActionInWindowRequest) => { const args: any[] = []; // If we run an action from the touchbar, we fill in the currently active resource @@ -162,7 +162,7 @@ export class ElectronWindow extends Themable { }); // Support resolve keybindings event - ipc.on('vscode:resolveKeybindings', (_event: any, rawActionIds: string) => { + ipc.on('vscode:resolveKeybindings', (event: any, rawActionIds: string) => { let actionIds: string[] = []; try { actionIds = JSON.parse(rawActionIds); @@ -178,7 +178,7 @@ export class ElectronWindow extends Themable { }, () => errors.onUnexpectedError); }); - ipc.on('vscode:reportError', (_event: any, error: string) => { + ipc.on('vscode:reportError', (event: any, error: string) => { if (error) { const errorParsed = JSON.parse(error); errorParsed.mainProcess = true; @@ -187,13 +187,13 @@ export class ElectronWindow extends Themable { }); // Support openFiles event for existing and new files - ipc.on('vscode:openFiles', (_event: any, request: IOpenFileRequest) => this.onOpenFiles(request)); + ipc.on('vscode:openFiles', (event: any, request: IOpenFileRequest) => this.onOpenFiles(request)); // Support addFolders event if we have a workspace opened - ipc.on('vscode:addFolders', (_event: any, request: IAddFoldersRequest) => this.onAddFoldersRequest(request)); + ipc.on('vscode:addFolders', (event: any, request: IAddFoldersRequest) => this.onAddFoldersRequest(request)); // Message support - ipc.on('vscode:showInfoMessage', (_event: any, message: string) => { + ipc.on('vscode:showInfoMessage', (event: any, message: string) => { this.notificationService.info(message); }); @@ -240,7 +240,7 @@ export class ElectronWindow extends Themable { }); // keyboard layout changed event - ipc.on('vscode:accessibilitySupportChanged', (_event: any, accessibilitySupportEnabled: boolean) => { + ipc.on('vscode:accessibilitySupportChanged', (event: any, accessibilitySupportEnabled: boolean) => { browser.setAccessibilitySupport(accessibilitySupportEnabled ? AccessibilitySupport.Enabled : AccessibilitySupport.Disabled); }); diff --git a/src/vs/workbench/electron-browser/workbench.ts b/src/vs/workbench/electron-browser/workbench.ts index 88d3f128bba..4ea24f29b42 100644 --- a/src/vs/workbench/electron-browser/workbench.ts +++ b/src/vs/workbench/electron-browser/workbench.ts @@ -35,8 +35,10 @@ import { WorkbenchLayout } from 'vs/workbench/browser/layout'; import { IActionBarRegistry, Extensions as ActionBarExtensions } from 'vs/workbench/browser/actions'; import { PanelRegistry, Extensions as PanelExtensions } from 'vs/workbench/browser/panel'; import { QuickOpenController } from 'vs/workbench/browser/parts/quickopen/quickOpenController'; +import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; +import { QuickInputService } from 'vs/workbench/browser/parts/quickinput/quickInput'; import { getServices } from 'vs/platform/instantiation/common/extensions'; -import { Position, Parts, IPartService, ILayoutOptions, Dimension } from 'vs/workbench/services/part/common/partService'; +import { Position, Parts, IPartService, ILayoutOptions, IDimension } from 'vs/workbench/services/part/common/partService'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { ContextMenuService } from 'vs/workbench/services/contextview/electron-browser/contextmenuService'; @@ -104,6 +106,11 @@ import { NotificationsAlerts } from 'vs/workbench/browser/parts/notifications/no import { NotificationsStatus } from 'vs/workbench/browser/parts/notifications/notificationsStatus'; import { registerNotificationCommands } from 'vs/workbench/browser/parts/notifications/notificationsCommands'; import { NotificationsToasts } from 'vs/workbench/browser/parts/notifications/notificationsToasts'; +import { IPCClient } from 'vs/base/parts/ipc/common/ipc'; +import { registerWindowDriver } from 'vs/platform/driver/electron-browser/driver'; +import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; +import { PreferencesService } from 'vs/workbench/services/preferences/browser/preferencesService'; +import { IInactiveExtensionUrlHandler, InactiveExtensionUrlHandler } from 'vs/platform/url/electron-browser/inactiveExtensionUrlHandler'; export const EditorsVisibleContext = new RawContextKey('editorIsOpen', false); export const InZenModeContext = new RawContextKey('inZenMode', false); @@ -199,6 +206,7 @@ export class Workbench implements IPartService { private editorPart: EditorPart; private statusbarPart: StatusbarPart; private quickOpen: QuickOpenController; + private quickInput: QuickInputService; private notificationsCenter: NotificationsCenter; private notificationsToasts: NotificationsToasts; private workbenchLayout: WorkbenchLayout; @@ -228,9 +236,10 @@ export class Workbench implements IPartService { constructor( parent: HTMLElement, container: HTMLElement, - configuration: IWindowConfiguration, + private configuration: IWindowConfiguration, serviceCollection: ServiceCollection, private lifecycleService: LifecycleService, + private mainProcessClient: IPCClient, @IInstantiationService private instantiationService: IInstantiationService, @IWorkspaceContextService private contextService: IWorkspaceContextService, @IStorageService private storageService: IStorageService, @@ -264,7 +273,7 @@ export class Workbench implements IPartService { return this._onTitleBarVisibilityChange.event; } - public get onEditorLayout(): Event { + public get onEditorLayout(): Event { return this.editorPart.onLayout; } @@ -315,6 +324,12 @@ export class Workbench implements IPartService { // Workbench Layout this.createWorkbenchLayout(); + // Driver + if (this.environmentService.driverHandle) { + registerWindowDriver(this.mainProcessClient, this.configuration.windowId, this.instantiationService) + .then(disposable => this.toUnbind.push(disposable)); + } + // Restore Parts return this.restoreParts(); } @@ -421,6 +436,7 @@ export class Workbench implements IPartService { workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(ToggleDevToolsAction, ToggleDevToolsAction.ID, ToggleDevToolsAction.LABEL, isDeveloping ? { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_I, mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_I } } : void 0), 'Developer: Toggle Developer Tools', localize('developer', "Developer")); workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(OpenRecentAction, OpenRecentAction.ID, OpenRecentAction.LABEL, { primary: isDeveloping ? null : KeyMod.CtrlCmd | KeyCode.KEY_R, mac: { primary: KeyMod.WinCtrl | KeyCode.KEY_R } }), 'File: Open Recent...', localize('file', "File")); workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(ReloadWindowWithExtensionsDisabledAction, ReloadWindowWithExtensionsDisabledAction.ID, ReloadWindowWithExtensionsDisabledAction.LABEL), 'Reload Window Without Extensions'); + // Actions for macOS native tabs management (only when enabled) const windowConfig = this.configurationService.getValue(); if (windowConfig && windowConfig.window && windowConfig.window.nativeTabs) { @@ -571,7 +587,7 @@ export class Workbench implements IPartService { // File Service this.fileService = this.instantiationService.createInstance(RemoteFileService); serviceCollection.set(IFileService, this.fileService); - this.toUnbind.push(this.fileService.onFileChanges(e => this.configurationService.handleWorkspaceFileEvents(e))); + this.configurationService.acquireFileService(this.fileService); // Editor service (editor part) this.editorPart = this.instantiationService.createInstance(EditorPart, Identifiers.EDITOR_PART, !this.hasFilesToCreateOpenOrDiff); @@ -601,6 +617,9 @@ export class Workbench implements IPartService { // SCM Service serviceCollection.set(ISCMService, new SyncDescriptor(SCMService)); + // Inactive extension URL handler + serviceCollection.set(IInactiveExtensionUrlHandler, new SyncDescriptor(InactiveExtensionUrlHandler)); + // Text Model Resolver Service serviceCollection.set(ITextModelService, new SyncDescriptor(TextModelResolverService)); @@ -622,6 +641,14 @@ export class Workbench implements IPartService { this.toUnbind.push({ dispose: () => this.quickOpen.shutdown() }); serviceCollection.set(IQuickOpenService, this.quickOpen); + // Quick input service + this.quickInput = this.instantiationService.createInstance(QuickInputService); + this.toUnbind.push({ dispose: () => this.quickInput.shutdown() }); + serviceCollection.set(IQuickInputService, this.quickInput); + + // PreferencesService + serviceCollection.set(IPreferencesService, this.instantiationService.createInstance(PreferencesService)); + // Contributed services const contributedServices = getServices(); for (let contributedService of contributedServices) { @@ -635,7 +662,7 @@ export class Workbench implements IPartService { this.instantiationService.createInstance(DefaultConfigurationExportHelper); - this.configurationService.setInstantiationService(this.getInstantiationService()); + this.configurationService.acquireInstantiationService(this.getInstantiationService()); } private initSettings(): void { @@ -712,7 +739,7 @@ export class Workbench implements IPartService { } public getContainer(part: Parts): HTMLElement { - let container: Builder = null; + let container: HTMLElement = null; switch (part) { case Parts.TITLEBAR_PART: container = this.titlebarPart.getContainer(); @@ -733,7 +760,8 @@ export class Workbench implements IPartService { container = this.statusbarPart.getContainer(); break; } - return container && container.getHTMLElement(); + + return container; } public isVisible(part: Parts): boolean { @@ -933,10 +961,10 @@ export class Workbench implements IPartService { this.sideBarPosition = position; // Adjust CSS - this.activitybarPart.getContainer().removeClass(oldPositionValue); - this.sidebarPart.getContainer().removeClass(oldPositionValue); - this.activitybarPart.getContainer().addClass(newPositionValue); - this.sidebarPart.getContainer().addClass(newPositionValue); + DOM.removeClass(this.activitybarPart.getContainer(), oldPositionValue); + DOM.removeClass(this.sidebarPart.getContainer(), oldPositionValue); + DOM.addClass(this.activitybarPart.getContainer(), newPositionValue); + DOM.addClass(this.sidebarPart.getContainer(), newPositionValue); // Update Styles this.activitybarPart.updateStyles(); @@ -958,8 +986,8 @@ export class Workbench implements IPartService { this.storageService.store(Workbench.panelPositionStorageKey, Position[this.panelPosition].toLowerCase(), StorageScope.WORKSPACE); // Adjust CSS - this.panelPart.getContainer().removeClass(oldPositionValue); - this.panelPart.getContainer().addClass(newPositionValue); + DOM.removeClass(this.panelPart.getContainer(), oldPositionValue); + DOM.addClass(this.panelPart.getContainer(), newPositionValue); // Update Styles this.panelPart.updateStyles(); @@ -1099,10 +1127,10 @@ export class Workbench implements IPartService { const editorContainer = this.editorPart.getContainer(); if (visibleEditors === 0) { this.editorsVisibleContext.reset(); - this.editorBackgroundDelayer.trigger(() => editorContainer.addClass('empty')); + this.editorBackgroundDelayer.trigger(() => DOM.addClass(editorContainer, 'empty')); } else { this.editorsVisibleContext.set(true); - this.editorBackgroundDelayer.trigger(() => editorContainer.removeClass('empty')); + this.editorBackgroundDelayer.trigger(() => DOM.removeClass(editorContainer, 'empty')); } } @@ -1143,8 +1171,8 @@ export class Workbench implements IPartService { private createWorkbenchLayout(): void { this.workbenchLayout = this.instantiationService.createInstance( WorkbenchLayout, - $(this.container), // Parent - this.workbench, // Workbench Container + this.container, // Parent + this.workbench.getHTMLElement(), // Workbench Container { titlebar: this.titlebarPart, // Title Bar activitybar: this.activitybarPart, // Activity Bar @@ -1154,6 +1182,7 @@ export class Workbench implements IPartService { statusbar: this.statusbarPart, // Statusbar }, this.quickOpen, // Quickopen + this.quickInput, // QuickInput this.notificationsCenter, // Notifications Center this.notificationsToasts // Notifications Toasts ); @@ -1208,7 +1237,7 @@ export class Workbench implements IPartService { role: 'contentinfo' }); - this.titlebarPart.create(titlebarContainer); + this.titlebarPart.create(titlebarContainer.getHTMLElement()); } private createActivityBarPart(): void { @@ -1219,7 +1248,7 @@ export class Workbench implements IPartService { role: 'navigation' }); - this.activitybarPart.create(activitybarPartContainer); + this.activitybarPart.create(activitybarPartContainer.getHTMLElement()); } private createSidebarPart(): void { @@ -1230,7 +1259,7 @@ export class Workbench implements IPartService { role: 'complementary' }); - this.sidebarPart.create(sidebarPartContainer); + this.sidebarPart.create(sidebarPartContainer.getHTMLElement()); } private createPanelPart(): void { @@ -1241,7 +1270,7 @@ export class Workbench implements IPartService { role: 'complementary' }); - this.panelPart.create(panelPartContainer); + this.panelPart.create(panelPartContainer.getHTMLElement()); } private createEditorPart(): void { @@ -1252,7 +1281,7 @@ export class Workbench implements IPartService { role: 'main' }); - this.editorPart.create(editorContainer); + this.editorPart.create(editorContainer.getHTMLElement()); } private createStatusbarPart(): void { @@ -1262,7 +1291,7 @@ export class Workbench implements IPartService { role: 'contentinfo' }); - this.statusbarPart.create(statusbarContainer); + this.statusbarPart.create(statusbarContainer.getHTMLElement()); } private createNotificationsHandlers(): void { diff --git a/src/vs/workbench/node/extensionHostMain.ts b/src/vs/workbench/node/extensionHostMain.ts index 9a17c7976dc..c2fe40a0852 100644 --- a/src/vs/workbench/node/extensionHostMain.ts +++ b/src/vs/workbench/node/extensionHostMain.ts @@ -17,7 +17,6 @@ import { QueryType, ISearchQuery } from 'vs/platform/search/common/search'; import { DiskSearch } from 'vs/workbench/services/search/node/searchService'; import { IInitData, IEnvironment, IWorkspaceData, MainContext } from 'vs/workbench/api/node/extHost.protocol'; import * as errors from 'vs/base/common/errors'; -import * as watchdog from 'native-watchdog'; import * as glob from 'vs/base/common/glob'; import { ExtensionActivatedByEvent } from 'vs/workbench/api/node/extHostExtensionActivator'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; @@ -26,16 +25,16 @@ import { RPCProtocol } from 'vs/workbench/services/extensions/node/rpcProtocol'; import URI from 'vs/base/common/uri'; import { ExtHostLogService } from 'vs/workbench/api/node/extHostLogService'; -// const nativeExit = process.exit.bind(process); +const nativeExit = process.exit.bind(process); function patchProcess(allowExit: boolean) { - process.exit = function (code) { + process.exit = function (code?: number) { if (allowExit) { exit(code); } else { const err = new Error('An extension called process.exit() and this was prevented.'); console.warn(err.stack); } - }; + } as (code?: number) => never; process.crash = function () { const err = new Error('An extension called process.crash() and this was prevented.'); @@ -43,14 +42,20 @@ function patchProcess(allowExit: boolean) { }; } export function exit(code?: number) { - //nativeExit(code); - // TODO@electron // See https://github.com/Microsoft/vscode/issues/32990 // calling process.exit() does not exit the process when the process is being debugged // It waits for the debugger to disconnect, but in our version, the debugger does not // receive an event that the process desires to exit such that it can disconnect. + let watchdog: { exit: (exitCode: number) => void; } = null; + try { + watchdog = require.__$__nodeRequire('native-watchdog'); + } catch (err) { + nativeExit(code); + return; + } + // Do exactly what node.js would have done, minus the wait for the debugger part if (code || code === 0) { diff --git a/src/vs/workbench/parts/cache/node/nodeCachedDataManager.ts b/src/vs/workbench/parts/cache/node/nodeCachedDataManager.ts index 77f1297d4ed..d7c1b98eca9 100644 --- a/src/vs/workbench/parts/cache/node/nodeCachedDataManager.ts +++ b/src/vs/workbench/parts/cache/node/nodeCachedDataManager.ts @@ -36,7 +36,7 @@ export class NodeCachedDataManager implements IWorkbenchContribution { /* __GDPR__ "cachedDataError" : { "errorCode" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, - "path": { "classification": "CustomerContent", "purpose": "PerformanceAndHealth" } + "path": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } } */ this._telemetryService.publicLog('cachedDataError', { @@ -55,12 +55,12 @@ export class NodeCachedDataManager implements IWorkbenchContribution { } */ this._telemetryService.publicLog('cachedDataInfo', { - didRequestCachedData: Boolean(global.require.getConfig().nodeCachedDataDir), + didRequestCachedData: Boolean((global).require.getConfig().nodeCachedDataDir), didRejectCachedData, didProduceCachedData }); - global.require.config({ onNodeCachedData: undefined }); + (global).require.config({ onNodeCachedData: undefined }); delete MonacoEnvironment.onNodeCachedData; } } diff --git a/src/vs/workbench/parts/codeEditor/codeEditor.contribution.ts b/src/vs/workbench/parts/codeEditor/codeEditor.contribution.ts index 42d312d8c7e..96f5b32b7f0 100644 --- a/src/vs/workbench/parts/codeEditor/codeEditor.contribution.ts +++ b/src/vs/workbench/parts/codeEditor/codeEditor.contribution.ts @@ -13,10 +13,4 @@ import './electron-browser/toggleMultiCursorModifier'; import './electron-browser/toggleRenderControlCharacter'; import './electron-browser/toggleRenderWhitespace'; import './electron-browser/toggleWordWrap'; -import { OPTIONS, TextBufferType } from 'vs/editor/common/model/textModel'; - -// Configure text buffer implementation -if (process.env['VSCODE_PIECE_TREE']) { - console.log(`Using TextBufferType.PieceTree (env variable VSCODE_PIECE_TREE)`); - OPTIONS.TEXT_BUFFER_IMPLEMENTATION = TextBufferType.PieceTree; -} +import './electron-browser/workbenchReferenceSearch'; \ No newline at end of file diff --git a/src/vs/workbench/parts/codeEditor/electron-browser/workbenchReferenceSearch.ts b/src/vs/workbench/parts/codeEditor/electron-browser/workbenchReferenceSearch.ts new file mode 100644 index 00000000000..ef42c2def1f --- /dev/null +++ b/src/vs/workbench/parts/codeEditor/electron-browser/workbenchReferenceSearch.ts @@ -0,0 +1,53 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { IEditorService } from 'vs/platform/editor/common/editor'; +import { IInstantiationService, optional } from 'vs/platform/instantiation/common/instantiation'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { IStorageService } from 'vs/platform/storage/common/storage'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; +import { ITextModelService } from 'vs/editor/common/services/resolverService'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { ReferencesController } from 'vs/editor/contrib/referenceSearch/referencesController'; + +export class WorkbenchReferencesController extends ReferencesController { + + public constructor( + editor: ICodeEditor, + @IContextKeyService contextKeyService: IContextKeyService, + @IEditorService editorService: IEditorService, + @ITextModelService textModelResolverService: ITextModelService, + @INotificationService notificationService: INotificationService, + @IInstantiationService instantiationService: IInstantiationService, + @IWorkspaceContextService contextService: IWorkspaceContextService, + @IStorageService storageService: IStorageService, + @IThemeService themeService: IThemeService, + @IConfigurationService configurationService: IConfigurationService, + @optional(IEnvironmentService) environmentService: IEnvironmentService + ) { + super( + false, + editor, + contextKeyService, + editorService, + textModelResolverService, + notificationService, + instantiationService, + contextService, + storageService, + themeService, + configurationService, + environmentService + ); + } +} + +registerEditorContribution(WorkbenchReferencesController); diff --git a/src/vs/workbench/parts/debug/browser/breakpointsView.ts b/src/vs/workbench/parts/debug/browser/breakpointsView.ts index f1413ed1e93..3c7afd4f1f6 100644 --- a/src/vs/workbench/parts/debug/browser/breakpointsView.ts +++ b/src/vs/workbench/parts/debug/browser/breakpointsView.ts @@ -124,8 +124,9 @@ export class BreakpointsView extends ViewsViewletPanel { const actions: IAction[] = []; const element = e.element; + const breakpointType = element instanceof Breakpoint && element.logMessage ? nls.localize('logPoint', "Log Point") : nls.localize('breakpoint', "Breakpoint"); if (element instanceof Breakpoint || element instanceof FunctionBreakpoint) { - actions.push(new Action('workbench.action.debug.openEditorAndEditBreakpoint', nls.localize('editConditionalBreakpoint', "Edit Breakpoint..."), undefined, true, () => { + actions.push(new Action('workbench.action.debug.openEditorAndEditBreakpoint', nls.localize('editBreakpoint', "Edit {0}...", breakpointType), undefined, true, () => { if (element instanceof Breakpoint) { return openBreakpointSource(element, false, false, this.debugService, this.editorService).then(editor => { const codeEditor = editor.getControl(); @@ -142,7 +143,7 @@ export class BreakpointsView extends ViewsViewletPanel { actions.push(new Separator()); } - actions.push(new RemoveBreakpointAction(RemoveBreakpointAction.ID, RemoveBreakpointAction.LABEL, this.debugService, this.keybindingService)); + actions.push(new RemoveBreakpointAction(RemoveBreakpointAction.ID, nls.localize('removeBreakpoint', "Remove {0}", breakpointType), this.debugService, this.keybindingService)); if (this.debugService.getModel().getBreakpoints().length + this.debugService.getModel().getFunctionBreakpoints().length > 1) { actions.push(new RemoveAllBreakpointsAction(RemoveAllBreakpointsAction.ID, RemoveAllBreakpointsAction.LABEL, this.debugService, this.keybindingService)); @@ -202,7 +203,7 @@ export class BreakpointsView extends ViewsViewletPanel { private get elements(): IEnablement[] { const model = this.debugService.getModel(); - const elements = (model.getExceptionBreakpoints()).concat(model.getFunctionBreakpoints()).concat(model.getBreakpoints()); + const elements = (>model.getExceptionBreakpoints()).concat(model.getFunctionBreakpoints()).concat(model.getBreakpoints()); return elements; } diff --git a/src/vs/workbench/parts/debug/browser/debugActions.ts b/src/vs/workbench/parts/debug/browser/debugActions.ts index ccb596753d1..d42736e6268 100644 --- a/src/vs/workbench/parts/debug/browser/debugActions.ts +++ b/src/vs/workbench/parts/debug/browser/debugActions.ts @@ -11,7 +11,7 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IFileService } from 'vs/platform/files/common/files'; -import { IDebugService, State, IProcess, IThread, IEnablement, IBreakpoint, IStackFrame, IExpression, REPL_ID, ProcessState } +import { IDebugService, State, IProcess, IThread, IEnablement, IBreakpoint, IStackFrame, REPL_ID, ProcessState } from 'vs/workbench/parts/debug/common/debug'; import { Variable, Expression, Thread, Breakpoint, Process } from 'vs/workbench/parts/debug/common/debugModel'; import { IPartService } from 'vs/workbench/services/part/common/partService'; @@ -215,10 +215,21 @@ export class RestartAction extends AbstractDebugAction { static LABEL = nls.localize('restartDebug', "Restart"); static RECONNECT_LABEL = nls.localize('reconnectDebug', "Reconnect"); - constructor(id: string, label: string, @IDebugService debugService: IDebugService, @IKeybindingService keybindingService: IKeybindingService) { + private startAction: StartAction; + + constructor(id: string, label: string, + @IDebugService debugService: IDebugService, + @IKeybindingService keybindingService: IKeybindingService, + @IWorkspaceContextService private contextService?: IWorkspaceContextService, + @IHistoryService historyService?: IHistoryService + ) { super(id, label, 'debug-action restart', debugService, keybindingService, 70); this.setLabel(this.debugService.getViewModel().focusedProcess); this.toDispose.push(this.debugService.getViewModel().onDidFocusStackFrame(() => this.setLabel(this.debugService.getViewModel().focusedProcess))); + + if (contextService !== undefined && historyService !== undefined) { + this.startAction = new StartAction(id, label, debugService, keybindingService, contextService, historyService); + } } private setLabel(process: IProcess): void { @@ -229,8 +240,9 @@ export class RestartAction extends AbstractDebugAction { if (!(process instanceof Process)) { process = this.debugService.getViewModel().focusedProcess; } + if (!process) { - return TPromise.as(null); + return this.startAction.run(); } if (this.debugService.getModel().getProcesses().length <= 1) { @@ -240,7 +252,11 @@ export class RestartAction extends AbstractDebugAction { } protected isEnabled(state: State): boolean { - return super.isEnabled(state) && (state === State.Running || state === State.Stopped); + return super.isEnabled(state) && ( + state === State.Running || + state === State.Stopped || + StartAction.isEnabled(this.debugService, this.contextService, this.debugService.getConfigurationManager().selectedConfiguration.name) + ); } } @@ -388,6 +404,27 @@ export class PauseAction extends AbstractDebugAction { } } +export class TerminateThreadAction extends AbstractDebugAction { + static readonly ID = 'workbench.action.debug.terminateThread'; + static LABEL = nls.localize('terminateThread', "Terminate Thread"); + + constructor(id: string, label: string, @IDebugService debugService: IDebugService, @IKeybindingService keybindingService: IKeybindingService) { + super(id, label, undefined, debugService, keybindingService); + } + + public run(thread: IThread): TPromise { + if (!(thread instanceof Thread)) { + thread = this.debugService.getViewModel().focusedThread; + } + + return thread ? thread.terminate() : TPromise.as(null); + } + + protected isEnabled(state: State): boolean { + return super.isEnabled(state) && (state === State.Running || state === State.Stopped); + } +} + export class RestartFrameAction extends AbstractDebugAction { static readonly ID = 'workbench.action.debug.restartFrame'; static LABEL = nls.localize('restartFrame', "Restart Frame"); @@ -453,7 +490,7 @@ export class EnableAllBreakpointsAction extends AbstractDebugAction { protected isEnabled(state: State): boolean { const model = this.debugService.getModel(); - return super.isEnabled(state) && (model.getBreakpoints()).concat(model.getFunctionBreakpoints()).concat(model.getExceptionBreakpoints()).some(bp => !bp.enabled); + return super.isEnabled(state) && (>model.getBreakpoints()).concat(model.getFunctionBreakpoints()).concat(model.getExceptionBreakpoints()).some(bp => !bp.enabled); } } @@ -472,7 +509,7 @@ export class DisableAllBreakpointsAction extends AbstractDebugAction { protected isEnabled(state: State): boolean { const model = this.debugService.getModel(); - return super.isEnabled(state) && (model.getBreakpoints()).concat(model.getFunctionBreakpoints()).concat(model.getExceptionBreakpoints()).some(bp => bp.enabled); + return super.isEnabled(state) && (>model.getBreakpoints()).concat(model.getFunctionBreakpoints()).concat(model.getExceptionBreakpoints()).some(bp => bp.enabled); } } @@ -600,15 +637,18 @@ export class AddToWatchExpressionsAction extends AbstractDebugAction { static readonly ID = 'workbench.debug.viewlet.action.addToWatchExpressions'; static LABEL = nls.localize('addToWatchExpressions', "Add to Watch"); - constructor(id: string, label: string, private expression: IExpression, @IDebugService debugService: IDebugService, @IKeybindingService keybindingService: IKeybindingService) { + constructor(id: string, label: string, private variable: Variable, @IDebugService debugService: IDebugService, @IKeybindingService keybindingService: IKeybindingService) { super(id, label, 'debug-action add-to-watch', debugService, keybindingService); + this.updateEnablement(); } public run(): TPromise { - const name = this.expression instanceof Variable ? this.expression.evaluateName : this.expression.name; - this.debugService.addWatchExpression(name); + this.debugService.addWatchExpression(this.variable.evaluateName); return TPromise.as(undefined); + } + protected isEnabled(state: State): boolean { + return super.isEnabled(state) && this.variable && !!this.variable.evaluateName; } } diff --git a/src/vs/workbench/parts/debug/browser/debugActionsWidget.ts b/src/vs/workbench/parts/debug/browser/debugActionsWidget.ts index 8eb08ca23f2..dcdbd5ae3d9 100644 --- a/src/vs/workbench/parts/debug/browser/debugActionsWidget.ts +++ b/src/vs/workbench/parts/debug/browser/debugActionsWidget.ts @@ -7,7 +7,7 @@ import 'vs/css!./media/debugActionsWidget'; import * as errors from 'vs/base/common/errors'; import * as strings from 'vs/base/common/strings'; import * as browser from 'vs/base/browser/browser'; -import * as builder from 'vs/base/browser/builder'; +import { $, Builder } from 'vs/base/browser/builder'; import * as dom from 'vs/base/browser/dom'; import * as arrays from 'vs/base/common/arrays'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; @@ -31,7 +31,6 @@ import { IContextViewService } from 'vs/platform/contextview/browser/contextView import { INotificationService } from 'vs/platform/notification/common/notification'; import { RunOnceScheduler } from 'vs/base/common/async'; -const $ = builder.$; const DEBUG_ACTIONS_WIDGET_POSITION_KEY = 'debug.actionswidgetposition'; export const debugToolBarBackground = registerColor('debugToolBar.background', { @@ -47,8 +46,8 @@ export const debugToolBarBorder = registerColor('debugToolBar.border', { export class DebugActionsWidget extends Themable implements IWorkbenchContribution { - private $el: builder.Builder; - private dragArea: builder.Builder; + private $el: Builder; + private dragArea: Builder; private actionBar: ActionBar; private allActions: AbstractDebugAction[]; private activeActions: AbstractDebugAction[]; @@ -79,7 +78,7 @@ export class DebugActionsWidget extends Themable implements IWorkbenchContributi this.$el.append(actionBarContainter); this.activeActions = []; - this.actionBar = new ActionBar(actionBarContainter, { + this.actionBar = new ActionBar(actionBarContainter.getHTMLElement(), { orientation: ActionsOrientation.HORIZONTAL, actionItemProvider: (action: IAction) => { if (action.id === FocusProcessAction.ID) { @@ -228,7 +227,7 @@ export class DebugActionsWidget extends Themable implements IWorkbenchContributi } if (!this.isBuilt) { this.isBuilt = true; - this.$el.build(builder.withElementById(this.partService.getWorkbenchElementId()).getHTMLElement()); + this.$el.build(document.getElementById(this.partService.getWorkbenchElementId())); } this.isVisible = true; diff --git a/src/vs/workbench/parts/debug/browser/debugCommands.ts b/src/vs/workbench/parts/debug/browser/debugCommands.ts index 98653f91eec..0055357ca3e 100644 --- a/src/vs/workbench/parts/debug/browser/debugCommands.ts +++ b/src/vs/workbench/parts/debug/browser/debugCommands.ts @@ -188,13 +188,13 @@ export function registerCommands(): void { const position = control.getPosition(); const modelUri = control.getModel().uri; const bp = debugService.getModel().getBreakpoints() - .filter(bp => bp.lineNumber === position.lineNumber && bp.column === position.column && bp.uri.toString() === modelUri.toString()).pop(); + .filter(bp => bp.lineNumber === position.lineNumber && (bp.column === position.column || !bp.column && position.column <= 1) && bp.uri.toString() === modelUri.toString()).pop(); if (bp) { return TPromise.as(null); } if (debugService.getConfigurationManager().canSetBreakpointsIn(control.getModel())) { - return debugService.addBreakpoints(modelUri, [{ lineNumber: position.lineNumber, column: position.column }]); + return debugService.addBreakpoints(modelUri, [{ lineNumber: position.lineNumber, column: position.column > 1 ? position.column : undefined }]); } } diff --git a/src/vs/workbench/parts/debug/browser/debugContentProvider.ts b/src/vs/workbench/parts/debug/browser/debugContentProvider.ts index 90cd905c6ca..7ca6eb20b7e 100644 --- a/src/vs/workbench/parts/debug/browser/debugContentProvider.ts +++ b/src/vs/workbench/parts/debug/browser/debugContentProvider.ts @@ -23,7 +23,7 @@ import { Source } from 'vs/workbench/parts/debug/common/debugSource'; * debug:arbitrary_path?session=123e4567-e89b-12d3-a456-426655440000&ref=1016 * \___/ \____________/ \__________________________________________/ \______/ * | | | | - * scheme source.path session id source.referencequery + * scheme source.path session id source.reference * * the arbitrary_path and the session id are encoded with 'encodeURIComponent' * @@ -58,7 +58,7 @@ export class DebugContentProvider implements IWorkbenchContribution, ITextModelC if (!process) { return TPromise.wrapError(new Error(localize('unable', "Unable to resolve the resource without a debug session"))); } - const source = process.sources.get(resource.toString()); + const source = process.getSourceForUri(resource); let rawSource: DebugProtocol.Source; if (source) { rawSource = source.raw; diff --git a/src/vs/workbench/parts/debug/browser/debugEditorActions.ts b/src/vs/workbench/parts/debug/browser/debugEditorActions.ts index 74b2f2b7c1f..a8342fec394 100644 --- a/src/vs/workbench/parts/debug/browser/debugEditorActions.ts +++ b/src/vs/workbench/parts/debug/browser/debugEditorActions.ts @@ -8,12 +8,11 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { KeyMod, KeyChord, KeyCode } from 'vs/base/common/keyCodes'; import { Range } from 'vs/editor/common/core/range'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; -import { ServicesAccessor, registerEditorAction, EditorAction, EditorCommand, registerEditorCommand } from 'vs/editor/browser/editorExtensions'; +import { ServicesAccessor, registerEditorAction, EditorAction } from 'vs/editor/browser/editorExtensions'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { IDebugService, CONTEXT_IN_DEBUG_MODE, CONTEXT_NOT_IN_DEBUG_REPL, CONTEXT_DEBUG_STATE, State, REPL_ID, VIEWLET_ID, IDebugEditorContribution, EDITOR_CONTRIBUTION_ID, CONTEXT_BREAKPOINT_WIDGET_VISIBLE, BreakpointWidgetContext } from 'vs/workbench/parts/debug/common/debug'; +import { IDebugService, CONTEXT_IN_DEBUG_MODE, CONTEXT_NOT_IN_DEBUG_REPL, CONTEXT_DEBUG_STATE, State, REPL_ID, VIEWLET_ID, IDebugEditorContribution, EDITOR_CONTRIBUTION_ID, BreakpointWidgetContext } from 'vs/workbench/parts/debug/common/debug'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; -import { KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; class ToggleBreakpointAction extends EditorAction { @@ -210,26 +209,6 @@ class ShowDebugHoverAction extends EditorAction { } } -class CloseBreakpointWidgetCommand extends EditorCommand { - - constructor() { - super({ - id: 'closeBreakpointWidget', - precondition: CONTEXT_BREAKPOINT_WIDGET_VISIBLE, - kbOpts: { - weight: KeybindingsRegistry.WEIGHT.editorContrib(8), - kbExpr: EditorContextKeys.focus, - primary: KeyCode.Escape, - secondary: [KeyMod.Shift | KeyCode.Escape] - } - }); - } - - public runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, args: any): void { - return editor.getContribution(EDITOR_CONTRIBUTION_ID).closeBreakpointWidget(); - } -} - registerEditorAction(ToggleBreakpointAction); registerEditorAction(ConditionalBreakpointAction); registerEditorAction(LogPointAction); @@ -237,4 +216,3 @@ registerEditorAction(RunToCursorAction); registerEditorAction(SelectionToReplAction); registerEditorAction(SelectionToWatchExpressionsAction); registerEditorAction(ShowDebugHoverAction); -registerEditorCommand(new CloseBreakpointWidgetCommand()); diff --git a/src/vs/workbench/parts/debug/browser/debugStatus.ts b/src/vs/workbench/parts/debug/browser/debugStatus.ts index 318a96be4f6..f72afca4b5d 100644 --- a/src/vs/workbench/parts/debug/browser/debugStatus.ts +++ b/src/vs/workbench/parts/debug/browser/debugStatus.ts @@ -90,8 +90,9 @@ export class DebugStatus extends Themable implements IStatusbarItem { this.icon = dom.append(a, $('.icon')); this.label = dom.append(a, $('span.label')); this.setLabel(); - this.updateStyles(); } + + this.updateStyles(); } private setLabel(): void { diff --git a/src/vs/workbench/parts/debug/browser/debugViewlet.ts b/src/vs/workbench/parts/debug/browser/debugViewlet.ts index b3bcf05b04c..35ee37d8769 100644 --- a/src/vs/workbench/parts/debug/browser/debugViewlet.ts +++ b/src/vs/workbench/parts/debug/browser/debugViewlet.ts @@ -5,7 +5,6 @@ import 'vs/css!./media/debugViewlet'; import * as nls from 'vs/nls'; -import { Builder } from 'vs/base/browser/builder'; import { Action, IAction } from 'vs/base/common/actions'; import * as DOM from 'vs/base/browser/dom'; import { TPromise } from 'vs/base/common/winjs.base'; @@ -56,11 +55,10 @@ export class DebugViewlet extends PersistentViewsViewlet { this._register(this.contextService.onDidChangeWorkbenchState(() => this.updateTitleArea())); } - async create(parent: Builder): TPromise { + async create(parent: HTMLElement): TPromise { await super.create(parent); - const el = parent.getHTMLElement(); - DOM.addClass(el, 'debug-viewlet'); + DOM.addClass(parent, 'debug-viewlet'); } public focus(): void { @@ -111,22 +109,26 @@ export class DebugViewlet extends PersistentViewsViewlet { } } - addPanel(panel: ViewsViewletPanel, size: number, index?: number): void { - super.addPanel(panel, size, index); + addPanels(panels: { panel: ViewsViewletPanel, size: number, index?: number }[]): void { + super.addPanels(panels); - // attach event listener to - if (panel.id === BREAKPOINTS_VIEW_ID) { - this.breakpointView = panel; - this.updateBreakpointsMaxSize(); - } else { - this.panelListeners.set(panel.id, panel.onDidChange(() => this.updateBreakpointsMaxSize())); + for (const { panel } of panels) { + // attach event listener to + if (panel.id === BREAKPOINTS_VIEW_ID) { + this.breakpointView = panel; + this.updateBreakpointsMaxSize(); + } else { + this.panelListeners.set(panel.id, panel.onDidChange(() => this.updateBreakpointsMaxSize())); + } } } - removePanel(panel: ViewsViewletPanel): void { - super.removePanel(panel); - dispose(this.panelListeners.get(panel.id)); - this.panelListeners.delete(panel.id); + removePanels(panels: ViewsViewletPanel[]): void { + super.removePanels(panels); + for (const panel of panels) { + dispose(this.panelListeners.get(panel.id)); + this.panelListeners.delete(panel.id); + } } private updateBreakpointsMaxSize(): void { diff --git a/src/vs/workbench/parts/debug/browser/media/breakpointWidget.css b/src/vs/workbench/parts/debug/browser/media/breakpointWidget.css index 8d190b91ca0..165dd70d690 100644 --- a/src/vs/workbench/parts/debug/browser/media/breakpointWidget.css +++ b/src/vs/workbench/parts/debug/browser/media/breakpointWidget.css @@ -14,28 +14,11 @@ justify-content: center; flex-direction: column; padding: 0 10px; + flex-shrink: 0; } -.monaco-editor .zone-widget .zone-widget-container.breakpoint-widget .inputBoxContainer { +.monaco-editor .zone-widget .zone-widget-container.breakpoint-widget .inputContainer { flex: 1; -} - -.monaco-editor .zone-widget .zone-widget-container.breakpoint-widget .monaco-inputbox { - border: none; -} - -.monaco-editor .breakpoint-widget .input { - font-family: Monaco, Menlo, Consolas, "Droid Sans Mono", "Inconsolata", "Courier New", monospace, "Droid Sans Fallback"; - line-height: 22px; - background-color: transparent; - padding: 8px; -} - -.monaco-workbench.mac .monaco-editor .breakpoint-widget .input { - font-size: 11px; -} - -.monaco-workbench.windows .monaco-editor .breakpoint-widget .input, -.monaco-workbench.linux .monaco-editor .breakpoint-widget .input { - font-size: 13px; + margin-top: 6px; + margin-bottom: 6px; } diff --git a/src/vs/workbench/parts/debug/common/debug.ts b/src/vs/workbench/parts/debug/common/debug.ts index 5d92e818732..566acd9d9af 100644 --- a/src/vs/workbench/parts/debug/common/debug.ts +++ b/src/vs/workbench/parts/debug/common/debug.ts @@ -20,6 +20,7 @@ import { Range, IRange } from 'vs/editor/common/core/range'; import { RawContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IDisposable } from 'vs/base/common/lifecycle'; export const VIEWLET_ID = 'workbench.view.debug'; export const VARIABLES_VIEW_ID = 'workbench.debug.variablesView'; @@ -37,6 +38,7 @@ export const CONTEXT_NOT_IN_DEBUG_REPL: ContextKeyExpr = CONTEXT_IN_DEBUG_REPL.t export const CONTEXT_ON_FIRST_DEBUG_REPL_LINE = new RawContextKey('onFirstDebugReplLine', false); export const CONTEXT_ON_LAST_DEBUG_REPL_LINE = new RawContextKey('onLastDebugReplLine', false); export const CONTEXT_BREAKPOINT_WIDGET_VISIBLE = new RawContextKey('breakpointWidgetVisible', false); +export const CONTEXT_IN_BREAKPOINT_WIDGET = new RawContextKey('inBreakpointWidget', false); export const CONTEXT_BREAKPOINTS_FOCUSED = new RawContextKey('breakpointsFocused', true); export const CONTEXT_WATCH_EXPRESSIONS_FOCUSED = new RawContextKey('watchExpressionsFocused', true); export const CONTEXT_VARIABLES_FOCUSED = new RawContextKey('variablesFocused', true); @@ -121,6 +123,7 @@ export interface ISession { stepOut(args: DebugProtocol.StepOutArguments): TPromise; continue(args: DebugProtocol.ContinueArguments): TPromise; pause(args: DebugProtocol.PauseArguments): TPromise; + terminateThreads(args: DebugProtocol.TerminateThreadsArguments): TPromise; stepBack(args: DebugProtocol.StepBackArguments): TPromise; reverseContinue(args: DebugProtocol.ReverseContinueArguments): TPromise; @@ -139,8 +142,8 @@ export interface IProcess extends ITreeElement { getName(includeRoot: boolean): string; configuration: IConfig; session: ISession; - sources: Map; state: ProcessState; + getSourceForUri(modelUri: uri): Source; getThread(threadId: number): IThread; getAllThreads(): IThread[]; getSource(raw: DebugProtocol.Source): Source; @@ -197,6 +200,7 @@ export interface IThread extends ITreeElement { stepBack(): TPromise; continue(): TPromise; pause(): TPromise; + terminate(): TPromise; reverseContinue(): TPromise; } @@ -255,6 +259,7 @@ export interface IBreakpoint extends IBaseBreakpoint { column: number; endColumn?: number; message: string; + adapterData: any; } export interface IFunctionBreakpoint extends IBaseBreakpoint { @@ -303,13 +308,13 @@ export interface IViewModel extends ITreeElement { } export interface IModel extends ITreeElement { - getProcesses(): IProcess[]; - getBreakpoints(): IBreakpoint[]; + getProcesses(): ReadonlyArray; + getBreakpoints(): ReadonlyArray; areBreakpointsActivated(): boolean; - getFunctionBreakpoints(): IFunctionBreakpoint[]; - getExceptionBreakpoints(): IExceptionBreakpoint[]; - getWatchExpressions(): IExpression[]; - getReplElements(): IReplElement[]; + getFunctionBreakpoints(): ReadonlyArray; + getExceptionBreakpoints(): ReadonlyArray; + getWatchExpressions(): ReadonlyArray; + getReplElements(): ReadonlyArray; onDidChangeBreakpoints: Event; onDidChangeCallStack: Event; @@ -345,6 +350,7 @@ export interface IDebugConfiguration { hideActionBar: boolean; showInStatusBar: 'never' | 'always' | 'onFirstSessionStart'; internalConsoleOptions: 'neverOpen' | 'openOnSessionStart' | 'openOnFirstSessionStart'; + extensionHostDebugAdapter: boolean; } export interface IGlobalConfig { @@ -354,23 +360,29 @@ export interface IGlobalConfig { } export interface IEnvConfig { - name?: string; - type: string; - request: string; internalConsoleOptions?: 'neverOpen' | 'openOnSessionStart' | 'openOnFirstSessionStart'; preLaunchTask?: string; postDebugTask?: string; - __restart?: any; - __sessionId?: string; debugServer?: number; noDebug?: boolean; - port?: number; } export interface IConfig extends IEnvConfig { + + // fundamental attributes + type: string; + request: string; + name?: string; + + // platform specifics windows?: IEnvConfig; osx?: IEnvConfig; linux?: IEnvConfig; + + // internals + __sessionId?: string; + __restart?: any; + port?: number; // TODO } export interface ICompound { @@ -378,34 +390,58 @@ export interface ICompound { configurations: (string | { name: string, folder: string })[]; } +export interface IDebugAdapter extends IDisposable { + readonly onError: Event; + readonly onExit: Event; + onRequest(callback: (request: DebugProtocol.Request) => void); + onEvent(callback: (event: DebugProtocol.Event) => void); + startSession(): TPromise; + sendMessage(message: DebugProtocol.ProtocolMessage): void; + sendResponse(response: DebugProtocol.Response): void; + sendRequest(command: string, args: any, clb: (result: DebugProtocol.Response) => void): void; + stopSession(): TPromise; +} + +export interface IDebugAdapterProvider extends ITerminalLauncher { + createDebugAdapter(debugType: string, adapterInfo: IAdapterExecutable | null): IDebugAdapter; + substituteVariables(folder: IWorkspaceFolder, config: IConfig): TPromise; +} + export interface IAdapterExecutable { command?: string; args?: string[]; } -export interface IRawEnvAdapter { - type?: string; - label?: string; +export interface IPlatformSpecificAdapterContribution { program?: string; args?: string[]; runtime?: string; runtimeArgs?: string[]; } -export interface IRawAdapter extends IRawEnvAdapter { +export interface IDebuggerContribution extends IPlatformSpecificAdapterContribution { + type?: string; + label?: string; + // debug adapter executable adapterExecutableCommand?: string; - enableBreakpointsFor?: { languageIds: string[] }; - configurationAttributes?: any; - configurationSnippets?: IJSONSchemaSnippet[]; - initialConfigurations?: any[]; - languages?: string[]; - variables?: { [key: string]: string }; + win?: IPlatformSpecificAdapterContribution; + winx86?: IPlatformSpecificAdapterContribution; + windows?: IPlatformSpecificAdapterContribution; + osx?: IPlatformSpecificAdapterContribution; + linux?: IPlatformSpecificAdapterContribution; + + // internal aiKey?: string; - win?: IRawEnvAdapter; - winx86?: IRawEnvAdapter; - windows?: IRawEnvAdapter; - osx?: IRawEnvAdapter; - linux?: IRawEnvAdapter; + + // supported languages + languages?: string[]; + enableBreakpointsFor?: { languageIds: string[] }; + + // debug configuration support + configurationAttributes?: any; + initialConfigurations?: any[]; + configurationSnippets?: IJSONSchemaSnippet[]; + variables?: { [key: string]: string }; } export interface IDebugConfigurationProvider { @@ -416,6 +452,25 @@ export interface IDebugConfigurationProvider { debugAdapterExecutable(folderUri: uri | undefined): TPromise; } +export interface ITerminalLauncher { + runInTerminal(args: DebugProtocol.RunInTerminalRequestArguments, config: ITerminalSettings): TPromise; +} + +export interface ITerminalSettings { + external: { + windowsExec: string, + osxExec: string, + linuxExec: string + }; + integrated: { + shell: { + osx: string, + windows: string, + linux: string + } + }; +} + export interface IConfigurationManager { /** * Returns true if breakpoints can be set for a given editor model. Depends on mode. @@ -446,6 +501,11 @@ export interface IConfigurationManager { resolveConfigurationByProviders(folderUri: uri | undefined, type: string | undefined, debugConfiguration: any): TPromise; debugAdapterExecutable(folderUri: uri | undefined, type: string): TPromise; + + registerDebugAdapterProvider(debugTypes: string[], debugAdapterLauncher: IDebugAdapterProvider): IDisposable; + createDebugAdapter(debugType: string, adapterExecutable: IAdapterExecutable | null): IDebugAdapter | undefined; + substituteVariables(debugType: string, folder: IWorkspaceFolder, config: IConfig): TPromise; + runInTerminal(debugType: string, args: DebugProtocol.RunInTerminalRequestArguments, config: ITerminalSettings): TPromise; } export interface ILaunch { @@ -488,12 +548,6 @@ export interface ILaunch { */ getConfigurationNames(includeCompounds?: boolean): string[]; - /** - * Returns the resolved configuration. - * Replaces os specific values, system variables, interactive variables. - */ - resolveConfiguration(config: IConfig): TPromise; - /** * Opens the launch.json file. Creates if it does not exist. */ diff --git a/src/vs/workbench/parts/debug/common/debugModel.ts b/src/vs/workbench/parts/debug/common/debugModel.ts index b07273aaf54..f15a2b56c47 100644 --- a/src/vs/workbench/parts/debug/common/debugModel.ts +++ b/src/vs/workbench/parts/debug/common/debugModel.ts @@ -20,7 +20,7 @@ import { ISuggestion } from 'vs/editor/common/modes'; import { Position } from 'vs/editor/common/core/position'; import { ITreeElement, IExpression, IExpressionContainer, IProcess, IStackFrame, IExceptionBreakpoint, IBreakpoint, IFunctionBreakpoint, IModel, IReplElementSource, - IConfig, ISession, IThread, IRawModelUpdate, IScope, IRawStoppedDetails, IEnablement, IBreakpointData, IExceptionInfo, IReplElement, ProcessState, IBreakpointsChangeEvent, IBreakpointUpdateData + IConfig, ISession, IThread, IRawModelUpdate, IScope, IRawStoppedDetails, IEnablement, IBreakpointData, IExceptionInfo, IReplElement, ProcessState, IBreakpointsChangeEvent, IBreakpointUpdateData, IBaseBreakpoint } from 'vs/workbench/parts/debug/common/debug'; import { Source } from 'vs/workbench/parts/debug/common/debugSource'; import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -525,6 +525,10 @@ export class Thread implements IThread { return this.process.session.pause({ threadId: this.threadId }); } + public terminate(): TPromise { + return this.process.session.terminateThreads({ threadIds: [this.threadId] }); + } + public reverseContinue(): TPromise { return this.process.session.reverseContinue({ threadId: this.threadId }); } @@ -532,8 +536,9 @@ export class Thread implements IThread { export class Process implements IProcess { - public sources: Map; + private sources: Map; private threads: Map; + public inactive = true; constructor(public configuration: IConfig, private _session: ISession & ITreeElement) { @@ -558,6 +563,10 @@ export class Process implements IProcess { return this.configuration.type === 'attach' ? ProcessState.ATTACH : ProcessState.LAUNCH; } + public getSourceForUri(modelUri: uri): Source { + return this.sources.get(modelUri.toString()); + } + public getSource(raw: DebugProtocol.Source): Source { let source = new Source(raw, this.getId()); if (this.sources.has(source.uri.toString())) { @@ -672,12 +681,48 @@ export class Process implements IProcess { return result; }, err => []); } + + setNotAvailable(modelUri: uri) { + const source = this.sources.get(modelUri.toString()); + if (source) { + source.available = false; + } + } } -export class Breakpoint implements IBreakpoint { +export class Enablement implements IEnablement { + constructor( + public enabled: boolean, + private id: string + ) { } + + public getId(): string { + return this.id; + } +} + +export class BaseBreakpoint extends Enablement implements IBaseBreakpoint { public verified: boolean; public idFromAdapter: number; + + constructor( + enabled: boolean, + public hitCondition: string, + public condition: string, + public logMessage: string, + id: string + ) { + super(enabled, id); + if (enabled === undefined) { + this.enabled = true; + } + this.verified = false; + } +} + +export class Breakpoint extends BaseBreakpoint implements IBreakpoint { + public message: string; public endLineNumber: number; public endColumn: number; @@ -686,48 +731,34 @@ export class Breakpoint implements IBreakpoint { public uri: uri, public lineNumber: number, public column: number, - public enabled: boolean, - public condition: string, - public hitCondition: string, - public logMessage: string, + enabled: boolean, + condition: string, + hitCondition: string, + logMessage: string, public adapterData: any, - private id = generateUuid() + id = generateUuid() ) { - if (enabled === undefined) { - this.enabled = true; - } - this.verified = false; - } - - public getId(): string { - return this.id; + super(enabled, hitCondition, condition, logMessage, id); } } -export class FunctionBreakpoint implements IFunctionBreakpoint { +export class FunctionBreakpoint extends BaseBreakpoint implements IFunctionBreakpoint { - public verified: boolean; - public idFromAdapter: number; - - constructor(public name: string, public enabled: boolean, public hitCondition: string, public condition: string, public logMessage: string, private id = generateUuid()) { - this.verified = false; - } - - public getId(): string { - return this.id; + constructor( + public name: string, + enabled: boolean, + hitCondition: string, + condition: string, + logMessage: string, + id = generateUuid()) { + super(enabled, hitCondition, condition, logMessage, id); } } -export class ExceptionBreakpoint implements IExceptionBreakpoint { +export class ExceptionBreakpoint extends Enablement implements IExceptionBreakpoint { - private id: string; - - constructor(public filter: string, public label: string, public enabled: boolean) { - this.id = generateUuid(); - } - - public getId(): string { - return this.id; + constructor(public filter: string, public label: string, enabled: boolean) { + super(enabled, generateUuid()); } } @@ -839,10 +870,23 @@ export class Model implements IModel { return thread.fetchCallStack(); } - public getBreakpoints(): Breakpoint[] { + public getBreakpoints(): IBreakpoint[] { return this.breakpoints; } + public getBreakpointsForResource(resource: uri): IBreakpoint[] { + const uriString = resource.toString(); + return this.breakpoints.filter(bp => bp.uri.toString() === uriString); + } + + public getEnabledBreakpointsForResource(resource: uri): IBreakpoint[] { + if (this.breakpointsActivated) { + const uriString = resource.toString(); + return this.breakpoints.filter(bp => bp.uri.toString() === uriString && bp.enabled); + } + return []; + } + public getFunctionBreakpoints(): IFunctionBreakpoint[] { return this.functionBreakpoints; } @@ -857,7 +901,7 @@ export class Model implements IModel { const ebp = this.exceptionBreakpoints.filter(ebp => ebp.filter === d.filter).pop(); return new ExceptionBreakpoint(d.filter, d.label, ebp ? ebp.enabled : d.default); }); - this._onDidChangeBreakpoints.fire(); + this._onDidChangeBreakpoints.fire({}); } } @@ -867,10 +911,10 @@ export class Model implements IModel { public setBreakpointsActivated(activated: boolean): void { this.breakpointsActivated = activated; - this._onDidChangeBreakpoints.fire(); + this._onDidChangeBreakpoints.fire({}); } - public addBreakpoints(uri: uri, rawData: IBreakpointData[], fireEvent = true): Breakpoint[] { + public addBreakpoints(uri: uri, rawData: IBreakpointData[], fireEvent = true): IBreakpoint[] { const newBreakpoints = rawData.map(rawBp => new Breakpoint(uri, rawBp.lineNumber, rawBp.column, rawBp.enabled, rawBp.condition, rawBp.hitCondition, rawBp.logMessage, undefined, rawBp.id)); this.breakpoints = this.breakpoints.concat(newBreakpoints); this.breakpointsActivated = true; @@ -975,7 +1019,7 @@ export class Model implements IModel { this._onDidChangeBreakpoints.fire({ changed: changed }); } - public addFunctionBreakpoint(functionName: string, id: string): FunctionBreakpoint { + public addFunctionBreakpoint(functionName: string, id: string): IFunctionBreakpoint { const newFunctionBreakpoint = new FunctionBreakpoint(functionName, true, undefined, undefined, undefined, id); this.functionBreakpoints.push(newFunctionBreakpoint); this._onDidChangeBreakpoints.fire({ added: [newFunctionBreakpoint] }); @@ -1096,11 +1140,7 @@ export class Model implements IModel { } public sourceIsNotAvailable(uri: uri): void { - this.processes.forEach(p => { - if (p.sources.has(uri.toString())) { - p.sources.get(uri.toString()).available = false; - } - }); + this.processes.forEach(p => p.setNotAvailable(uri)); this._onDidChangeCallStack.fire(); } diff --git a/src/vs/workbench/parts/debug/common/debugSource.ts b/src/vs/workbench/parts/debug/common/debugSource.ts index a1c372db5a0..c2ff61f69fe 100644 --- a/src/vs/workbench/parts/debug/common/debugSource.ts +++ b/src/vs/workbench/parts/debug/common/debugSource.ts @@ -15,6 +15,20 @@ import { Schemas } from 'vs/base/common/network'; const UNKNOWN_SOURCE_LABEL = nls.localize('unknownSource', "Unknown Source"); +/** + * Debug URI format + * + * a debug URI represents a Source object and the debug session where the Source comes from. + * + * debug:arbitrary_path?session=123e4567-e89b-12d3-a456-426655440000&ref=1016 + * \___/ \____________/ \__________________________________________/ \______/ + * | | | | + * scheme source.path session id source.reference + * + * the arbitrary_path and the session id are encoded with 'encodeURIComponent' + * + */ + export class Source { public readonly uri: uri; @@ -30,8 +44,9 @@ export class Source { this.uri = uri.parse(`${DEBUG_SCHEME}:${encodeURIComponent(path)}?session=${encodeURIComponent(sessionId)}&ref=${this.raw.sourceReference}`); } else { if (paths.isAbsolute(path)) { - this.uri = uri.file(path); // path should better be absolute! + this.uri = uri.file(path); } else { + // assume that path is a URI this.uri = uri.parse(path); } } diff --git a/src/vs/workbench/parts/debug/electron-browser/breakpointWidget.ts b/src/vs/workbench/parts/debug/electron-browser/breakpointWidget.ts index d046222b670..037df6816e5 100644 --- a/src/vs/workbench/parts/debug/electron-browser/breakpointWidget.ts +++ b/src/vs/workbench/parts/debug/electron-browser/breakpointWidget.ts @@ -6,26 +6,48 @@ import 'vs/css!../browser/media/breakpointWidget'; import * as nls from 'vs/nls'; import * as errors from 'vs/base/common/errors'; -import { KeyCode } from 'vs/base/common/keyCodes'; -import { isWindows, isMacintosh } from 'vs/base/common/platform'; +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { SelectBox } from 'vs/base/browser/ui/selectBox/selectBox'; import * as lifecycle from 'vs/base/common/lifecycle'; import * as dom from 'vs/base/browser/dom'; -import { InputBox } from 'vs/base/browser/ui/inputbox/inputBox'; +import { Position } from 'vs/editor/common/core/position'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { ZoneWidget } from 'vs/editor/contrib/zoneWidget/zoneWidget'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; -import { IDebugService, IBreakpoint, BreakpointWidgetContext as Context } from 'vs/workbench/parts/debug/common/debug'; -import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; -import { once } from 'vs/base/common/functional'; -import { attachInputBoxStyler, attachSelectBoxStyler } from 'vs/platform/theme/common/styler'; +import { IDebugService, IBreakpoint, BreakpointWidgetContext as Context, CONTEXT_BREAKPOINT_WIDGET_VISIBLE, DEBUG_SCHEME, IDebugEditorContribution, EDITOR_CONTRIBUTION_ID, CONTEXT_IN_BREAKPOINT_WIDGET } from 'vs/workbench/parts/debug/common/debug'; +import { attachSelectBoxStyler } from 'vs/platform/theme/common/styler'; import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { SimpleDebugEditor } from 'vs/workbench/parts/debug/electron-browser/simpleDebugEditor'; +import { createDecorator, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { ServicesAccessor, EditorCommand, registerEditorCommand } from 'vs/editor/browser/editorExtensions'; +import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; +import { IModelService } from 'vs/editor/common/services/modelService'; +import uri from 'vs/base/common/uri'; +import { SuggestRegistry, ISuggestResult, SuggestContext } from 'vs/editor/common/modes'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { ITextModel } from 'vs/editor/common/model'; +import { wireCancellationToken } from 'vs/base/common/async'; +import { provideSuggestionItems } from 'vs/editor/contrib/suggest/suggest'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { transparent, editorForeground } from 'vs/platform/theme/common/colorRegistry'; +import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { IDecorationOptions } from 'vs/editor/common/editorCommon'; const $ = dom.$; +const IPrivateBreakopintWidgetService = createDecorator('privateBreakopintWidgetService'); +export interface IPrivateBreakopintWidgetService { + _serviceBrand: any; + close(success: boolean): void; +} +const DECORATION_KEY = 'breakpointwidgetdecoration'; -export class BreakpointWidget extends ZoneWidget { +export class BreakpointWidget extends ZoneWidget implements IPrivateBreakopintWidgetService { + public _serviceBrand: any; - private inputBox: InputBox; + private selectContainer: HTMLElement; + private input: SimpleDebugEditor; private toDispose: lifecycle.IDisposable[]; private conditionInput = ''; private hitCountInput = ''; @@ -35,13 +57,17 @@ export class BreakpointWidget extends ZoneWidget { constructor(editor: ICodeEditor, private lineNumber: number, private column: number, private context: Context, @IContextViewService private contextViewService: IContextViewService, @IDebugService private debugService: IDebugService, - @IThemeService private themeService: IThemeService + @IThemeService private themeService: IThemeService, + @IContextKeyService private contextKeyService: IContextKeyService, + @IInstantiationService private instantiationService: IInstantiationService, + @IModelService private modelService: IModelService, + @ICodeEditorService private codeEditorService: ICodeEditorService, ) { super(editor, { showFrame: true, showArrow: false, frameWidth: 1 }); this.toDispose = []; const uri = this.editor.getModel().uri; - this.breakpoint = this.debugService.getModel().getBreakpoints().filter(bp => bp.lineNumber === this.lineNumber && bp.column === this.column && bp.uri.toString() === uri.toString()).pop(); + this.breakpoint = this.debugService.getModel().getBreakpoints().filter(bp => bp.lineNumber === this.lineNumber && bp.uri.toString() === uri.toString()).pop(); if (this.context === undefined) { if (this.breakpoint && !this.breakpoint.condition && !this.breakpoint.hitCondition && this.breakpoint.logMessage) { @@ -58,6 +84,8 @@ export class BreakpointWidget extends ZoneWidget { this.dispose(); } })); + this.codeEditorService.registerDecorationType(DECORATION_KEY, {}); + this.create(); } @@ -72,17 +100,6 @@ export class BreakpointWidget extends ZoneWidget { } } - private get ariaLabel(): string { - switch (this.context) { - case Context.LOG_MESSAGE: - return nls.localize('breakpointWidgetLogMessageAriaLabel', "The program will log this message everytime this breakpoint is hit. Press Enter to accept or Escape to cancel."); - case Context.HIT_COUNT: - return nls.localize('breakpointWidgetHitCountAriaLabel', "The program will only stop here if the hit count is met. Press Enter to accept or Escape to cancel."); - default: - return nls.localize('breakpointWidgetAriaLabel', "The program will only stop here if this condition is true. Press Enter to accept or Escape to cancel."); - } - } - private getInputValue(breakpoint: IBreakpoint): string { switch (this.context) { case Context.LOG_MESSAGE: @@ -95,15 +112,16 @@ export class BreakpointWidget extends ZoneWidget { } private rememberInput(): void { + const value = this.input.getModel().getValue(); switch (this.context) { case Context.LOG_MESSAGE: - this.logMessageInput = this.inputBox.value; + this.logMessageInput = value; break; case Context.HIT_COUNT: - this.hitCountInput = this.inputBox.value; + this.hitCountInput = value; break; default: - this.conditionInput = this.inputBox.value; + this.conditionInput = value; } } @@ -111,89 +129,199 @@ export class BreakpointWidget extends ZoneWidget { this.setCssClass('breakpoint-widget'); const selectBox = new SelectBox([nls.localize('expression', "Expression"), nls.localize('hitCount', "Hit Count"), nls.localize('logMessage', "Log Message")], this.context, this.contextViewService); this.toDispose.push(attachSelectBoxStyler(selectBox, this.themeService)); - selectBox.render(dom.append(container, $('.breakpoint-select-container'))); + this.selectContainer = $('.breakpoint-select-container'); + selectBox.render(dom.append(container, this.selectContainer)); selectBox.onDidSelect(e => { this.rememberInput(); this.context = e.index; - this.inputBox.setAriaLabel(this.ariaLabel); - this.inputBox.setPlaceHolder(this.placeholder); - this.inputBox.value = this.getInputValue(this.breakpoint); + const value = this.getInputValue(this.breakpoint); + this.input.getModel().setValue(value); }); - const inputBoxContainer = dom.append(container, $('.inputBoxContainer')); - this.inputBox = new InputBox(inputBoxContainer, this.contextViewService, { - placeholder: this.placeholder, - ariaLabel: this.ariaLabel - }); - this.toDispose.push(attachInputBoxStyler(this.inputBox, this.themeService)); - this.toDispose.push(this.inputBox); + this.createBreakpointInput(dom.append(container, $('.inputContainer'))); - dom.addClass(this.inputBox.inputElement, isWindows ? 'windows' : isMacintosh ? 'mac' : 'linux'); - this.inputBox.value = this.getInputValue(this.breakpoint); + this.input.getModel().setValue(this.getInputValue(this.breakpoint)); + this.input.setPosition({ lineNumber: 1, column: this.input.getModel().getLineMaxColumn(1) }); // Due to an electron bug we have to do the timeout, otherwise we do not get focus - setTimeout(() => this.inputBox.focus(), 0); + setTimeout(() => this.input.focus(), 100); + } - let disposed = false; - const wrapUp = once((success: boolean) => { - if (!disposed) { - disposed = true; - if (success) { - // if there is already a breakpoint on this location - remove it. + public close(success: boolean): void { + if (success) { + // if there is already a breakpoint on this location - remove it. - let condition = this.breakpoint && this.breakpoint.condition; - let hitCondition = this.breakpoint && this.breakpoint.hitCondition; - let logMessage = this.breakpoint && this.breakpoint.logMessage; - this.rememberInput(); + let condition = this.breakpoint && this.breakpoint.condition; + let hitCondition = this.breakpoint && this.breakpoint.hitCondition; + let logMessage = this.breakpoint && this.breakpoint.logMessage; + this.rememberInput(); - if (this.conditionInput) { - condition = this.conditionInput; - } - if (this.hitCountInput) { - hitCondition = this.hitCountInput; - } - if (this.logMessageInput) { - logMessage = this.logMessageInput; - } + if (this.conditionInput) { + condition = this.conditionInput; + } + if (this.hitCountInput) { + hitCondition = this.hitCountInput; + } + if (this.logMessageInput) { + logMessage = this.logMessageInput; + } - if (this.breakpoint) { - this.debugService.updateBreakpoints(this.breakpoint.uri, { - [this.breakpoint.getId()]: { - condition, - hitCondition, - verified: this.breakpoint.verified, - logMessage - } - }, false); - } else { - this.debugService.addBreakpoints(this.editor.getModel().uri, [{ - lineNumber: this.lineNumber, - column: this.breakpoint ? this.breakpoint.column : undefined, - enabled: true, - condition, - hitCondition, - logMessage - }]).done(null, errors.onUnexpectedError); + if (this.breakpoint) { + this.debugService.updateBreakpoints(this.breakpoint.uri, { + [this.breakpoint.getId()]: { + condition, + hitCondition, + verified: this.breakpoint.verified, + column: this.breakpoint.column, + logMessage } + }, false); + } else { + this.debugService.addBreakpoints(this.editor.getModel().uri, [{ + lineNumber: this.lineNumber, + enabled: true, + condition, + hitCondition, + logMessage + }]).done(null, errors.onUnexpectedError); + } + } + + this.dispose(); + } + + protected _doLayout(heightInPixel: number, widthInPixel: number): void { + this.input.layout({ height: 18, width: widthInPixel - 113 }); + } + + private createBreakpointInput(container: HTMLElement): void { + const scopedContextKeyService = this.contextKeyService.createScoped(container); + this.toDispose.push(scopedContextKeyService); + + const scopedInstatiationService = this.instantiationService.createChild(new ServiceCollection( + [IContextKeyService, scopedContextKeyService], [IPrivateBreakopintWidgetService, this])); + + const options = SimpleDebugEditor.getEditorOptions(); + this.input = scopedInstatiationService.createInstance(SimpleDebugEditor, container, options); + CONTEXT_IN_BREAKPOINT_WIDGET.bindTo(scopedContextKeyService).set(true); + const model = this.modelService.createModel('', null, uri.parse(`${DEBUG_SCHEME}:breakpointinput`), true); + this.input.setModel(model); + this.toDispose.push(model); + const setDecorations = () => { + const value = this.input.getModel().getValue(); + const decorations = !!value ? [] : this.createDecorations(); + this.input.setDecorations(DECORATION_KEY, decorations); + }; + this.input.getModel().onDidChangeContent(() => setDecorations()); + this.themeService.onThemeChange(() => setDecorations()); + + this.toDispose.push(SuggestRegistry.register({ scheme: DEBUG_SCHEME, hasAccessToAllModels: true }, { + provideCompletionItems: (model: ITextModel, position: Position, _context: SuggestContext, token: CancellationToken): Thenable => { + let suggestionsPromise: TPromise; + if (this.context === Context.CONDITION || this.context === Context.LOG_MESSAGE && this.isCurlyBracketOpen()) { + suggestionsPromise = provideSuggestionItems(this.editor.getModel(), new Position(this.lineNumber, this.column), 'none', undefined, _context).then(suggestions => { + return { + suggestions: suggestions.map(s => { + if (this.context === Context.CONDITION) { + s.suggestion.overwriteBefore = position.column - 1; + s.suggestion.overwriteAfter = 0; + } + + return s.suggestion; + }) + }; + }); + } else { + suggestionsPromise = TPromise.as({ suggestions: [] }); } - this.dispose(); - } - }); - - this.toDispose.push(dom.addStandardDisposableListener(this.inputBox.inputElement, 'keydown', (e: IKeyboardEvent) => { - const isEscape = e.equals(KeyCode.Escape); - const isEnter = e.equals(KeyCode.Enter); - if (isEscape || isEnter) { - e.stopPropagation(); - wrapUp(isEnter); + return wireCancellationToken(token, suggestionsPromise); } })); } + private createDecorations(): IDecorationOptions[] { + return [{ + range: { + startLineNumber: 0, + endLineNumber: 0, + startColumn: 0, + endColumn: 1 + }, + renderOptions: { + after: { + contentText: this.placeholder, + color: transparent(editorForeground, 0.4)(this.themeService.getTheme()).toString() + } + } + }]; + } + + private isCurlyBracketOpen(): boolean { + const value = this.input.getModel().getValue(); + for (let i = this.input.getPosition().column - 2; i >= 0; i--) { + if (value[i] === '{') { + return true; + } + + if (value[i] === '}') { + return false; + } + } + + return false; + } + public dispose(): void { super.dispose(); + this.input.dispose(); lifecycle.dispose(this.toDispose); setTimeout(() => this.editor.focus(), 0); } } + +class AcceptBreakpointWidgetInputAction extends EditorCommand { + + constructor() { + super({ + id: 'breakpointWidget.action.acceptInput', + precondition: CONTEXT_BREAKPOINT_WIDGET_VISIBLE, + kbOpts: { + kbExpr: CONTEXT_IN_BREAKPOINT_WIDGET, + primary: KeyCode.Enter + } + }); + } + + public runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor): void { + accessor.get(IPrivateBreakopintWidgetService).close(true); + } +} + +class CloseBreakpointWidgetCommand extends EditorCommand { + + constructor() { + super({ + id: 'closeBreakpointWidget', + precondition: CONTEXT_BREAKPOINT_WIDGET_VISIBLE, + kbOpts: { + kbExpr: EditorContextKeys.textInputFocus, + primary: KeyCode.Escape, + secondary: [KeyMod.Shift | KeyCode.Escape] + } + }); + } + + public runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, args: any): void { + const debugContribution = editor.getContribution(EDITOR_CONTRIBUTION_ID); + if (debugContribution) { + // if focus is in outer editor we need to use the debug contribution to close + return debugContribution.closeBreakpointWidget(); + } + + accessor.get(IPrivateBreakopintWidgetService).close(false); + } +} + +registerEditorCommand(new AcceptBreakpointWidgetInputAction()); +registerEditorCommand(new CloseBreakpointWidgetCommand()); diff --git a/src/vs/workbench/parts/debug/electron-browser/callStackView.ts b/src/vs/workbench/parts/debug/electron-browser/callStackView.ts index 8e3b75e7f39..6703b2b0c70 100644 --- a/src/vs/workbench/parts/debug/electron-browser/callStackView.ts +++ b/src/vs/workbench/parts/debug/electron-browser/callStackView.ts @@ -18,7 +18,7 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { BaseDebugController, twistiePixels, renderViewTree } from 'vs/workbench/parts/debug/browser/baseDebugView'; import { ITree, IActionProvider, IDataSource, IRenderer, IAccessibilityProvider } from 'vs/base/parts/tree/browser/tree'; import { IAction, IActionItem } from 'vs/base/common/actions'; -import { RestartAction, StopAction, ContinueAction, StepOverAction, StepIntoAction, StepOutAction, PauseAction, RestartFrameAction } from 'vs/workbench/parts/debug/browser/debugActions'; +import { RestartAction, StopAction, ContinueAction, StepOverAction, StepIntoAction, StepOutAction, PauseAction, RestartFrameAction, TerminateThreadAction } from 'vs/workbench/parts/debug/browser/debugActions'; import { CopyStackTraceAction } from 'vs/workbench/parts/debug/electron-browser/electronDebugActions'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; @@ -27,6 +27,7 @@ import { basenameOrAuthority } from 'vs/base/common/resources'; import { TreeResourceNavigator, WorkbenchTree } from 'vs/platform/list/browser/listService'; import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { Separator } from 'vs/base/browser/ui/actionbar/actionbar'; const $ = dom.$; @@ -268,6 +269,9 @@ class CallStackActionProvider implements IActionProvider { } else { actions.push(new PauseAction(PauseAction.ID, PauseAction.LABEL, this.debugService, this.keybindingService)); } + + actions.push(new Separator()); + actions.push(new TerminateThreadAction(TerminateThreadAction.ID, TerminateThreadAction.LABEL, this.debugService, this.keybindingService)); } else if (element instanceof StackFrame) { if (element.thread.process.session.capabilities.supportsRestartFrame) { actions.push(new RestartFrameAction(RestartFrameAction.ID, RestartFrameAction.LABEL, this.debugService, this.keybindingService)); diff --git a/src/vs/workbench/parts/debug/electron-browser/debug.contribution.ts b/src/vs/workbench/parts/debug/electron-browser/debug.contribution.ts index d808ca71e7e..796f6c5e59f 100644 --- a/src/vs/workbench/parts/debug/electron-browser/debug.contribution.ts +++ b/src/vs/workbench/parts/debug/electron-browser/debug.contribution.ts @@ -30,7 +30,7 @@ import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; import { DebugEditorModelManager } from 'vs/workbench/parts/debug/browser/debugEditorModelManager'; import { StepOverAction, ClearReplAction, FocusReplAction, StepIntoAction, StepOutAction, StartAction, RestartAction, ContinueAction, StopAction, DisconnectAction, PauseAction, AddFunctionBreakpointAction, - ConfigureAction, DisableAllBreakpointsAction, EnableAllBreakpointsAction, RemoveAllBreakpointsAction, RunAction, ReapplyBreakpointsAction, SelectAndStartAction + ConfigureAction, DisableAllBreakpointsAction, EnableAllBreakpointsAction, RemoveAllBreakpointsAction, RunAction, ReapplyBreakpointsAction, SelectAndStartAction, TerminateThreadAction } from 'vs/workbench/parts/debug/browser/debugActions'; import { DebugActionsWidget } from 'vs/workbench/parts/debug/browser/debugActionsWidget'; import * as service from 'vs/workbench/parts/debug/electron-browser/debugService'; @@ -86,7 +86,7 @@ Registry.as(ViewletExtensions.Viewlets).registerViewlet(new Vie VIEWLET_ID, nls.localize('debug', "Debug"), 'debug', - 40 + 3 )); const openViewletKb: IKeybindings = { @@ -134,6 +134,7 @@ registry.registerWorkbenchAction(new SyncActionDescriptor(StopAction, StopAction registry.registerWorkbenchAction(new SyncActionDescriptor(DisconnectAction, DisconnectAction.ID, DisconnectAction.LABEL), 'Debug: Disconnect', debugCategory); registry.registerWorkbenchAction(new SyncActionDescriptor(ContinueAction, ContinueAction.ID, ContinueAction.LABEL, { primary: KeyCode.F5 }, CONTEXT_IN_DEBUG_MODE), 'Debug: Continue', debugCategory); registry.registerWorkbenchAction(new SyncActionDescriptor(PauseAction, PauseAction.ID, PauseAction.LABEL, { primary: KeyCode.F6 }, CONTEXT_IN_DEBUG_MODE), 'Debug: Pause', debugCategory); +registry.registerWorkbenchAction(new SyncActionDescriptor(TerminateThreadAction, TerminateThreadAction.ID, TerminateThreadAction.LABEL, undefined, CONTEXT_IN_DEBUG_MODE), 'Debug: Terminate Thread', debugCategory); registry.registerWorkbenchAction(new SyncActionDescriptor(ConfigureAction, ConfigureAction.ID, ConfigureAction.LABEL), 'Debug: Open launch.json', debugCategory); registry.registerWorkbenchAction(new SyncActionDescriptor(AddFunctionBreakpointAction, AddFunctionBreakpointAction.ID, AddFunctionBreakpointAction.LABEL), 'Debug: Add Function Breakpoint', debugCategory); registry.registerWorkbenchAction(new SyncActionDescriptor(ReapplyBreakpointsAction, ReapplyBreakpointsAction.ID, ReapplyBreakpointsAction.LABEL), 'Debug: Reapply All Breakpoints', debugCategory); @@ -209,6 +210,11 @@ configurationRegistry.registerConfiguration({ description: nls.localize({ comment: ['This is the description for a setting'], key: 'launch' }, "Global debug launch configuration. Should be used as an alternative to 'launch.json' that is shared across workspaces"), default: { configurations: [], compounds: [] }, $ref: launchSchemaId + }, + 'debug.extensionHostDebugAdapter': { + type: 'boolean', + description: nls.localize({ comment: ['This is the description for a setting'], key: 'extensionHostDebugAdapter' }, "Run debug adapter in extension host"), + default: false } } }); diff --git a/src/vs/workbench/parts/debug/electron-browser/debugConfigurationManager.ts b/src/vs/workbench/parts/debug/electron-browser/debugConfigurationManager.ts index 92dcad553fb..1428ee55ed1 100644 --- a/src/vs/workbench/parts/debug/electron-browser/debugConfigurationManager.ts +++ b/src/vs/workbench/parts/debug/electron-browser/debugConfigurationManager.ts @@ -8,7 +8,6 @@ import { dispose, IDisposable } from 'vs/base/common/lifecycle'; import { Event, Emitter } from 'vs/base/common/event'; import { TPromise } from 'vs/base/common/winjs.base'; import * as strings from 'vs/base/common/strings'; -import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; import * as objects from 'vs/base/common/objects'; import uri from 'vs/base/common/uri'; import * as paths from 'vs/base/common/paths'; @@ -26,17 +25,18 @@ import { IFileService } from 'vs/platform/files/common/files'; import { IWorkspaceContextService, IWorkspaceFolder, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { IDebugConfigurationProvider, IRawAdapter, ICompound, IDebugConfiguration, IConfig, IEnvConfig, IGlobalConfig, IConfigurationManager, ILaunch, IAdapterExecutable } from 'vs/workbench/parts/debug/common/debug'; -import { Adapter } from 'vs/workbench/parts/debug/node/debugAdapter'; +import { IDebugConfigurationProvider, IDebuggerContribution, ICompound, IDebugConfiguration, IConfig, IGlobalConfig, IConfigurationManager, ILaunch, IAdapterExecutable, IDebugAdapterProvider, IDebugAdapter, ITerminalSettings, ITerminalLauncher } from 'vs/workbench/parts/debug/common/debug'; +import { Debugger } from 'vs/workbench/parts/debug/node/debugger'; import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IQuickOpenService } from 'vs/platform/quickOpen/common/quickOpen'; import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; import { launchSchemaId } from 'vs/workbench/services/configuration/common/configuration'; -import { IPreferencesService } from 'vs/workbench/parts/preferences/common/preferences'; +import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; +import { TerminalLauncher } from 'vs/workbench/parts/debug/electron-browser/terminalSupport'; // debuggers extension point -export const debuggersExtPoint = extensionsRegistry.ExtensionsRegistry.registerExtensionPoint('debuggers', [], { +export const debuggersExtPoint = extensionsRegistry.ExtensionsRegistry.registerExtensionPoint('debuggers', [], { description: nls.localize('vscode.extension.contributes.debuggers', 'Contributes debug adapters.'), type: 'array', defaultSnippets: [{ body: [{ type: '', extensions: [] }] }], @@ -221,7 +221,7 @@ const DEBUG_SELECTED_CONFIG_NAME_KEY = 'debug.selectedconfigname'; const DEBUG_SELECTED_ROOT = 'debug.selectedroot'; export class ConfigurationManager implements IConfigurationManager { - private adapters: Adapter[]; + private debuggers: Debugger[]; private breakpointModeIdsSet = new Set(); private launches: ILaunch[]; private selectedName: string; @@ -229,6 +229,9 @@ export class ConfigurationManager implements IConfigurationManager { private toDispose: IDisposable[]; private _onDidSelectConfigurationName = new Emitter(); private providers: IDebugConfigurationProvider[]; + private debugAdapterProviders: Map; + private terminalLauncher: ITerminalLauncher; + constructor( @IWorkspaceContextService private contextService: IWorkspaceContextService, @@ -238,10 +241,11 @@ export class ConfigurationManager implements IConfigurationManager { @IInstantiationService private instantiationService: IInstantiationService, @ICommandService private commandService: ICommandService, @IStorageService private storageService: IStorageService, - @ILifecycleService lifecycleService: ILifecycleService + @ILifecycleService lifecycleService: ILifecycleService, + @IConfigurationResolverService private configurationResolverService: IConfigurationResolverService ) { this.providers = []; - this.adapters = []; + this.debuggers = []; this.toDispose = []; this.registerListeners(lifecycleService); this.initLaunches(); @@ -250,6 +254,7 @@ export class ConfigurationManager implements IConfigurationManager { if (previousSelectedLaunch) { this.selectConfiguration(previousSelectedLaunch, this.storageService.get(DEBUG_SELECTED_CONFIG_NAME_KEY, StorageScope.WORKSPACE)); } + this.debugAdapterProviders = new Map(); } public registerDebugConfigurationProvider(handle: number, debugConfigurationProvider: IDebugConfigurationProvider): void { @@ -260,10 +265,10 @@ export class ConfigurationManager implements IConfigurationManager { debugConfigurationProvider.handle = handle; this.providers = this.providers.filter(p => p.handle !== handle); this.providers.push(debugConfigurationProvider); - const adapter = this.getAdapter(debugConfigurationProvider.type); + const dbg = this.getDebugger(debugConfigurationProvider.type); // Check if the provider contributes provideDebugConfigurations method - if (adapter && debugConfigurationProvider.provideDebugConfigurations) { - adapter.hasConfigurationProvider = true; + if (dbg && debugConfigurationProvider.provideDebugConfigurations) { + dbg.hasConfigurationProvider = true; } } @@ -300,12 +305,53 @@ export class ConfigurationManager implements IConfigurationManager { return TPromise.as(undefined); } + public registerDebugAdapterProvider(debugTypes: string[], debugAdapterLauncher: IDebugAdapterProvider): IDisposable { + debugTypes.forEach(debugType => this.debugAdapterProviders.set(debugType, debugAdapterLauncher)); + return { + dispose: () => { + debugTypes.forEach(debugType => this.debugAdapterProviders.delete(debugType)); + } + }; + } + + private getDebugAdapterProvider(type: string): IDebugAdapterProvider | undefined { + return this.debugAdapterProviders.get(type); + } + + public createDebugAdapter(debugType: string, adapterExecutable: IAdapterExecutable): IDebugAdapter | undefined { + let dap = this.getDebugAdapterProvider(debugType); + if (dap) { + return dap.createDebugAdapter(debugType, adapterExecutable); + } + return undefined; + } + + public substituteVariables(debugType: string, folder: IWorkspaceFolder, config: IConfig): TPromise { + let dap = this.getDebugAdapterProvider(debugType); + if (dap) { + return dap.substituteVariables(folder, config); + } + return TPromise.as(config); + } + + public runInTerminal(debugType: string, args: DebugProtocol.RunInTerminalRequestArguments, config: ITerminalSettings): TPromise { + + let tl: ITerminalLauncher = this.getDebugAdapterProvider(debugType); + if (!tl) { + if (!this.terminalLauncher) { + this.terminalLauncher = this.instantiationService.createInstance(TerminalLauncher); + } + tl = this.terminalLauncher; + } + return tl.runInTerminal(args, config); + } + private registerListeners(lifecycleService: ILifecycleService): void { debuggersExtPoint.setHandler((extensions) => { extensions.forEach(extension => { extension.value.forEach(rawAdapter => { if (!rawAdapter.type || (typeof rawAdapter.type !== 'string')) { - extension.collector.error(nls.localize('debugNoType', "Debug adapter 'type' can not be omitted and must be of type 'string'.")); + extension.collector.error(nls.localize('debugNoType', "Debugger 'type' can not be omitted and must be of type 'string'.")); } if (rawAdapter.enableBreakpointsFor) { rawAdapter.enableBreakpointsFor.languageIds.forEach(modeId => { @@ -313,17 +359,17 @@ export class ConfigurationManager implements IConfigurationManager { }); } - const duplicate = this.adapters.filter(a => a.type === rawAdapter.type).pop(); + const duplicate = this.getDebugger(rawAdapter.type); if (duplicate) { duplicate.merge(rawAdapter, extension.description); } else { - this.adapters.push(new Adapter(this, rawAdapter, extension.description, this.configurationService, this.commandService)); + this.debuggers.push(new Debugger(this, rawAdapter, extension.description, this.configurationService, this.commandService, this.configurationResolverService)); } }); }); // update the schema to include all attributes, snippets and types from extensions. - this.adapters.forEach(adapter => { + this.debuggers.forEach(adapter => { const items = (schema.properties['configurations'].items); const schemaAttributes = adapter.getSchemaAttributes(); if (schemaAttributes) { @@ -448,24 +494,24 @@ export class ConfigurationManager implements IConfigurationManager { return this.breakpointModeIdsSet.has(modeId); } - public getAdapter(type: string): Adapter { - return this.adapters.filter(adapter => strings.equalsIgnoreCase(adapter.type, type)).pop(); + public getDebugger(type: string): Debugger { + return this.debuggers.filter(dbg => strings.equalsIgnoreCase(dbg.type, type)).pop(); } - public guessAdapter(type?: string): TPromise { + public guessDebugger(type?: string): TPromise { if (type) { - const adapter = this.getAdapter(type); + const adapter = this.getDebugger(type); return TPromise.as(adapter); } const editor = this.editorService.getActiveEditor(); - let candidates: Adapter[]; + let candidates: Debugger[]; if (editor) { const codeEditor = editor.getControl(); if (isCodeEditor(codeEditor)) { const model = codeEditor.getModel(); const language = model ? model.getLanguageIdentifier().language : undefined; - const adapters = this.adapters.filter(a => a.languages && a.languages.indexOf(language) >= 0); + const adapters = this.debuggers.filter(a => a.languages && a.languages.indexOf(language) >= 0); if (adapters.length === 1) { return TPromise.as(adapters[0]); } @@ -476,11 +522,11 @@ export class ConfigurationManager implements IConfigurationManager { } if (!candidates) { - candidates = this.adapters.filter(a => a.hasInitialConfiguration() || a.hasConfigurationProvider); + candidates = this.debuggers.filter(a => a.hasInitialConfiguration() || a.hasConfigurationProvider); } return this.quickOpenService.pick([...candidates, { label: 'More...', separator: { border: true } }], { placeHolder: nls.localize('selectDebug', "Select Environment") }) .then(picked => { - if (picked instanceof Adapter) { + if (picked instanceof Debugger) { return picked; } if (picked) { @@ -510,7 +556,6 @@ class Launch implements ILaunch { @IFileService private fileService: IFileService, @IWorkbenchEditorService protected editorService: IWorkbenchEditorService, @IConfigurationService protected configurationService: IConfigurationService, - @IConfigurationResolverService private configurationResolverService: IConfigurationResolverService, @IWorkspaceContextService protected contextService: IWorkspaceContextService, @IExtensionService private extensionService: IExtensionService ) { @@ -569,41 +614,6 @@ class Launch implements ILaunch { return config.configurations.filter(config => config && config.name === name).shift(); } - protected getWorkspaceForResolving(): IWorkspaceFolder { - if (this.workspace) { - return this.workspace; - } - - if (this.contextService.getWorkspace().folders.length === 1) { - return this.contextService.getWorkspace().folders[0]; - } - - return undefined; - } - - public resolveConfiguration(config: IConfig): TPromise { - const result = objects.deepClone(config) as IConfig; - // Set operating system specific properties #1873 - const setOSProperties = (flag: boolean, osConfig: IEnvConfig) => { - if (flag && osConfig) { - Object.keys(osConfig).forEach(key => { - result[key] = osConfig[key]; - }); - } - }; - setOSProperties(isWindows, result.windows); - setOSProperties(isMacintosh, result.osx); - setOSProperties(isLinux, result.linux); - - // massage configuration attributes - append workspace path to relatvie paths, substitute variables in paths. - Object.keys(result).forEach(key => { - result[key] = this.configurationResolverService.resolveAny(this.getWorkspaceForResolving(), result[key]); - }); - - const adapter = this.configurationManager.getAdapter(result.type); - return this.configurationResolverService.resolveInteractiveVariables(result, adapter ? adapter.variables : null); - } - public openConfigFile(sideBySide: boolean, type?: string): TPromise { return this.extensionService.activateByEvent('onDebugInitialConfigurations').then(() => this.extensionService.activateByEvent('onDebug').then(() => { const resource = this.uri; @@ -613,7 +623,7 @@ class Launch implements ILaunch { // launch.json not found: create one by collecting launch configs from debugConfigProviders - return this.configurationManager.guessAdapter(type).then(adapter => { + return this.configurationManager.guessDebugger(type).then(adapter => { if (adapter) { return this.configurationManager.provideDebugConfigurations(this.workspace.uri, adapter.type).then(initialConfigs => { return adapter.getInitialConfigurationContent(initialConfigs); @@ -673,7 +683,7 @@ class WorkspaceLaunch extends Launch implements ILaunch { @IWorkspaceContextService contextService: IWorkspaceContextService, @IExtensionService extensionService: IExtensionService ) { - super(configurationManager, undefined, fileService, editorService, configurationService, configurationResolverService, contextService, extensionService); + super(configurationManager, undefined, fileService, editorService, configurationService, contextService, extensionService); } get uri(): uri { @@ -705,7 +715,7 @@ class UserLaunch extends Launch implements ILaunch { @IWorkspaceContextService contextService: IWorkspaceContextService, @IExtensionService extensionService: IExtensionService ) { - super(configurationManager, undefined, fileService, editorService, configurationService, configurationResolverService, contextService, extensionService); + super(configurationManager, undefined, fileService, editorService, configurationService, contextService, extensionService); } get uri(): uri { diff --git a/src/vs/workbench/parts/debug/electron-browser/debugEditorContribution.ts b/src/vs/workbench/parts/debug/electron-browser/debugEditorContribution.ts index b700d3e313d..0b5f26ba2ed 100644 --- a/src/vs/workbench/parts/debug/electron-browser/debugEditorContribution.ts +++ b/src/vs/workbench/parts/debug/electron-browser/debugEditorContribution.ts @@ -11,6 +11,7 @@ import * as lifecycle from 'vs/base/common/lifecycle'; import * as env from 'vs/base/common/platform'; import uri from 'vs/base/common/uri'; import { visit } from 'vs/base/common/json'; +import severity from 'vs/base/common/severity'; import { Constants } from 'vs/editor/common/core/uint'; import { IAction, Action } from 'vs/base/common/actions'; import { KeyCode } from 'vs/base/common/keyCodes'; @@ -43,6 +44,7 @@ import { IMarginData } from 'vs/editor/browser/controller/mouseTarget'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ContextSubMenu } from 'vs/base/browser/contextmenu'; import { memoize } from 'vs/base/common/decorators'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; const HOVER_DELAY = 300; const LAUNCH_JSON_REGEX = /launch\.json$/; @@ -77,7 +79,8 @@ export class DebugEditorContribution implements IDebugEditorContribution { @ITelemetryService private telemetryService: ITelemetryService, @IConfigurationService private configurationService: IConfigurationService, @IThemeService themeService: IThemeService, - @IKeybindingService private keybindingService: IKeybindingService + @IKeybindingService private keybindingService: IKeybindingService, + @IDialogService private dialogService: IDialogService, ) { this.breakpointHintDecoration = []; this.hoverWidget = new DebugHoverWidget(this.editor, this.debugService, this.instantiationService, themeService); @@ -92,10 +95,11 @@ export class DebugEditorContribution implements IDebugEditorContribution { private getContextMenuActions(breakpoints: IBreakpoint[], uri: uri, lineNumber: number): TPromise<(IAction | ContextSubMenu)[]> { const actions: (IAction | ContextSubMenu)[] = []; if (breakpoints.length === 1) { - actions.push(new RemoveBreakpointAction(RemoveBreakpointAction.ID, RemoveBreakpointAction.LABEL, this.debugService, this.keybindingService)); + const breakpointType = breakpoints[0].logMessage ? nls.localize('logPoint', "Log Point") : nls.localize('breakpoint', "Breakpoint"); + actions.push(new RemoveBreakpointAction(RemoveBreakpointAction.ID, nls.localize('removeBreakpoint', "Remove {0}", breakpointType), this.debugService, this.keybindingService)); actions.push(new Action( 'workbench.debug.action.editBreakpointAction', - nls.localize('editBreakpoint', "Edit Breakpoint..."), + nls.localize('editBreakpoint', "Edit {0}...", breakpointType), undefined, true, () => TPromise.as(this.editor.getContribution(EDITOR_CONTRIBUTION_ID).showBreakpointWidget(breakpoints[0].lineNumber, breakpoints[0].column)) @@ -103,7 +107,7 @@ export class DebugEditorContribution implements IDebugEditorContribution { actions.push(new Action( `workbench.debug.viewlet.action.toggleBreakpoint`, - breakpoints[0].enabled ? nls.localize('disableBreakpoint', "Disable Breakpoint") : nls.localize('enableBreakpoint', "Enable Breakpoint"), + breakpoints[0].enabled ? nls.localize('disableBreakpoint', "Disable {0}", breakpointType) : nls.localize('enableBreakpoint', "Enable {0}", breakpointType), undefined, true, () => this.debugService.enableOrDisableBreakpoints(!breakpoints[0].enabled, breakpoints[0]) @@ -145,7 +149,7 @@ export class DebugEditorContribution implements IDebugEditorContribution { )); actions.push(new Action( 'addConditionalBreakpoint', - nls.localize('conditionalBreakpoint', "Add Conditional Breakpoint..."), + nls.localize('addConditionalBreakpoint', "Add Conditional Breakpoint..."), null, true, () => TPromise.as(this.editor.getContribution(EDITOR_CONTRIBUTION_ID).showBreakpointWidget(lineNumber, undefined)) @@ -190,7 +194,25 @@ export class DebugEditorContribution implements IDebugEditorContribution { .filter(bp => bp.uri.toString() === uri.toString() && bp.lineNumber === lineNumber); if (breakpoints.length) { - breakpoints.forEach(bp => this.debugService.removeBreakpoints(bp.getId())); + if (breakpoints.some(bp => !!bp.condition || !!bp.logMessage || !!bp.hitCondition)) { + const logPoint = breakpoints.every(bp => !!bp.logMessage); + const breakpointType = logPoint ? nls.localize('logPoint', "Log Point") : nls.localize('breakpoint', "Breakpoint"); + this.dialogService.show(severity.Info, nls.localize('breakpointHasCondition', "This {0} has a valuable {1} that will get lost on remove. Consider disabling the {0} instead.", + breakpointType.toLowerCase(), logPoint ? nls.localize('message', "message") : nls.localize('condition', "condition")), [ + nls.localize('removeLogPoint', "Remove {0}", breakpointType), + nls.localize('disableLogPoint', "Disable {0}", breakpointType), + nls.localize('cancel', "Cancel") + ], { cancelId: 2 }).then(choice => { + if (choice === 0) { + breakpoints.forEach(bp => this.debugService.removeBreakpoints(bp.getId())); + } + if (choice === 1) { + breakpoints.forEach(bp => this.debugService.enableOrDisableBreakpoints(false, bp)); + } + }); + } else { + breakpoints.forEach(bp => this.debugService.removeBreakpoints(bp.getId())); + } } else if (canSetBreakpoints) { this.debugService.addBreakpoints(uri, [{ lineNumber }]); } diff --git a/src/vs/workbench/parts/debug/electron-browser/debugHover.ts b/src/vs/workbench/parts/debug/electron-browser/debugHover.ts index e6248b7f74e..c91609db06a 100644 --- a/src/vs/workbench/parts/debug/electron-browser/debugHover.ts +++ b/src/vs/workbench/parts/debug/electron-browser/debugHover.ts @@ -27,6 +27,7 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { editorHoverBackground, editorHoverBorder } from 'vs/platform/theme/common/colorRegistry'; import { WorkbenchTree, WorkbenchTreeController } from 'vs/platform/list/browser/listService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; const $ = dom.$; const MAX_ELEMENTS_SHOWN = 18; @@ -208,15 +209,17 @@ export class DebugHoverWidget implements IContentWidget { this.highlightDecorations = this.editor.deltaDecorations(this.highlightDecorations, [{ range: new Range(pos.lineNumber, expressionRange.startColumn, pos.lineNumber, expressionRange.startColumn + matchingExpression.length), - options: { - className: 'hoverHighlight' - } + options: DebugHoverWidget._HOVER_HIGHLIGHT_DECORATION_OPTIONS }]); return this.doShow(pos, expression, focus); }); } + private static _HOVER_HIGHLIGHT_DECORATION_OPTIONS = ModelDecorationOptions.register({ + className: 'hoverHighlight' + }); + private doFindExpression(container: IExpressionContainer, namesToFind: string[]): TPromise { if (!container) { return TPromise.as(null); diff --git a/src/vs/workbench/parts/debug/electron-browser/debugService.ts b/src/vs/workbench/parts/debug/electron-browser/debugService.ts index 276910af561..d24ae70b037 100644 --- a/src/vs/workbench/parts/debug/electron-browser/debugService.ts +++ b/src/vs/workbench/parts/debug/electron-browser/debugService.ts @@ -82,7 +82,6 @@ export class DebugService implements debug.IDebugService { private debugType: IContextKey; private debugState: IContextKey; private breakpointsToSendOnResourceSaved: Set; - private launchJsonChanged: boolean; private firstSessionStart: boolean; private skipRunningTask: boolean; private previousState: debug.State; @@ -154,11 +153,17 @@ export class DebugService implements debug.IDebugService { const session = process.session; if (broadcast.channel === EXTENSION_ATTACH_BROADCAST_CHANNEL) { - this.onSessionEnd(session); - + const initialAttach = process.configuration.request === 'launch'; process.configuration.request = 'attach'; process.configuration.port = broadcast.payload.port; - this.doCreateProcess(process.session.root, process.configuration, process.getId()); + // Do not end process on initial attach (since the request is still 'launch') + if (initialAttach) { + session.attach(process.configuration); + } else { + this.onSessionEnd(session); + this.doCreateProcess(process.session.root, process.configuration, process.getId()); + } + return; } @@ -352,10 +357,10 @@ export class DebugService implements debug.IDebugService { const outputSeverity = event.body.category === 'stderr' ? severity.Error : event.body.category === 'console' ? severity.Warning : severity.Info; if (event.body.category === 'telemetry') { - // only log telemetry events from debug adapter if the adapter provided the telemetry key + // only log telemetry events from debug adapter if the debug extension provided the telemetry key // and the user opted in telemetry if (session.customTelemetryService && this.telemetryService.isOptedIn) { - // __GDPR__TODO__ We're sending events in the name of the debug adapter and we can not ensure that those are declared correctly. + // __GDPR__TODO__ We're sending events in the name of the debug extension and we can not ensure that those are declared correctly. session.customTelemetryService.publicLog(event.body.output, event.body.data); } @@ -698,7 +703,6 @@ export class DebugService implements debug.IDebugService { this.allProcesses.clear(); this.model.getBreakpoints().forEach(bp => bp.verified = false); } - this.launchJsonChanged = false; let config: debug.IConfig, compound: debug.ICompound; if (!configOrName) { @@ -766,7 +770,7 @@ export class DebugService implements debug.IDebugService { config.noDebug = true; } - return (type ? TPromise.as(null) : this.configurationManager.guessAdapter().then(a => type = a && a.type)).then(() => + return (type ? TPromise.as(null) : this.configurationManager.guessDebugger().then(a => type = a && a.type)).then(() => (type ? this.extensionService.activateByEvent(`onDebugResolve:${type}`) : TPromise.as(null)).then(() => this.configurationManager.resolveConfigurationByProviders(launch && launch.workspace ? launch.workspace.uri : undefined, type, config).then(config => { // a falsy config indicates an aborted launch @@ -786,15 +790,38 @@ export class DebugService implements debug.IDebugService { }); } + private substituteVariables(launch: debug.ILaunch, config: debug.IConfig): TPromise { + const dbg = this.configurationManager.getDebugger(config.type); + if (dbg) { + let folder: IWorkspaceFolder = undefined; + if (launch.workspace) { + folder = launch.workspace; + } else { + const folders = this.contextService.getWorkspace().folders; + if (folders.length === 1) { + folder = folders[0]; + } + } + return dbg.substituteVariables(folder, config).then(config => { + return config; + }, (err: Error) => { + this.showError(err.message); + return undefined; // bail out + }); + } + return TPromise.as(config); + } + private createProcess(launch: debug.ILaunch, config: debug.IConfig, sessionId: string): TPromise { return this.textFileService.saveAll().then(() => - (launch ? launch.resolveConfiguration(config) : TPromise.as(config)).then(resolvedConfig => { + this.substituteVariables(launch, config).then(resolvedConfig => { + if (!resolvedConfig) { // User canceled resolving of interactive variables, silently return return undefined; } - if (!this.configurationManager.getAdapter(resolvedConfig.type) || (config.request !== 'attach' && config.request !== 'launch')) { + if (!this.configurationManager.getDebugger(resolvedConfig.type) || (config.request !== 'attach' && config.request !== 'launch')) { let message: string; if (config.request !== 'attach' && config.request !== 'launch') { message = config.request ? nls.localize('debugRequestNotSupported', "Attribute '{0}' has an unsupported value '{1}' in the chosen debug configuration.", 'request', config.request) @@ -855,9 +882,9 @@ export class DebugService implements debug.IDebugService { telemetryInfo['common.vscodesessionid'] = info.sessionId; return telemetryInfo; }).then(data => { - const adapter = this.configurationManager.getAdapter(configuration.type); - const { aiKey, type } = adapter; - const publisher = adapter.extensionDescription.publisher; + const dbg = this.configurationManager.getDebugger(configuration.type); + const { aiKey, type } = dbg; + const publisher = dbg.extensionDescription.publisher; let client: TelemetryClient; let customTelemetryService: TelemetryService; @@ -882,7 +909,7 @@ export class DebugService implements debug.IDebugService { customTelemetryService = new TelemetryService({ appender }, this.configurationService); } - const session = this.instantiationService.createInstance(RawDebugSession, sessionId, configuration.debugServer, adapter, customTelemetryService, root); + const session = this.instantiationService.createInstance(RawDebugSession, sessionId, configuration.debugServer, dbg, customTelemetryService, root); const process = this.model.addProcess(configuration, session); this.allProcesses.set(process.getId(), process); @@ -946,8 +973,8 @@ export class DebugService implements debug.IDebugService { breakpointCount: this.model.getBreakpoints().length, exceptionBreakpoints: this.model.getExceptionBreakpoints(), watchExpressionsCount: this.model.getWatchExpressions().length, - extensionName: adapter.extensionDescription.id, - isBuiltin: adapter.extensionDescription.isBuiltin, + extensionName: dbg.extensionDescription.id, + isBuiltin: dbg.extensionDescription.isBuiltin, launchJsonExists: root && !!this.configurationService.getValue('launch', { resource: root.uri }) }); }).then(() => process, (error: Error | string) => { @@ -1081,12 +1108,9 @@ export class DebugService implements debug.IDebugService { return new TPromise((c, e) => { setTimeout(() => { - // Read the configuration again if a launch.json has been changed, if not just use the inmemory configuration let config = process.configuration; - const launch = process.session.root ? this.configurationManager.getLaunch(process.session.root.uri) : undefined; - if (this.launchJsonChanged && launch) { - this.launchJsonChanged = false; + if (launch) { config = launch.getConfiguration(process.configuration.name) || config; // Take the type from the process since the debug extension might overwrite it #21316 config.type = process.configuration.type; @@ -1149,7 +1173,9 @@ export class DebugService implements debug.IDebugService { process.inactive = true; this._onDidEndProcess.fire(process); if (process.configuration.postDebugTask) { - this.runTask(process.getId(), process.session.root, process.configuration.postDebugTask); + this.runTask(process.getId(), process.session.root, process.configuration.postDebugTask).done(undefined, err => + this.notificationService.error(err) + ); } } @@ -1205,9 +1231,9 @@ export class DebugService implements debug.IDebugService { return TPromise.as(null); } - const breakpointsToSend = this.model.getBreakpoints().filter(bp => this.model.areBreakpointsActivated() && bp.enabled && bp.uri.toString() === modelUri.toString()); + const breakpointsToSend = this.model.getEnabledBreakpointsForResource(modelUri); - const source = process.sources.get(modelUri.toString()); + const source = process.getSourceForUri(modelUri); let rawSource: DebugProtocol.Source; if (source) { rawSource = source.raw; @@ -1303,13 +1329,9 @@ export class DebugService implements debug.IDebugService { } fileChangesEvent.getUpdated().forEach(event => { - if (this.breakpointsToSendOnResourceSaved.has(event.resource.toString())) { - this.breakpointsToSendOnResourceSaved.delete(event.resource.toString()); + if (this.breakpointsToSendOnResourceSaved.delete(event.resource.toString())) { this.sendBreakpoints(event.resource, true).done(null, errors.onUnexpectedError); } - if (event.resource.toString().indexOf('.vscode/launch.json') >= 0) { - this.launchJsonChanged = true; - } }); } diff --git a/src/vs/workbench/parts/debug/electron-browser/electronDebugActions.ts b/src/vs/workbench/parts/debug/electron-browser/electronDebugActions.ts index 4709acbb680..e38f831bf0e 100644 --- a/src/vs/workbench/parts/debug/electron-browser/electronDebugActions.ts +++ b/src/vs/workbench/parts/debug/electron-browser/electronDebugActions.ts @@ -18,6 +18,7 @@ export class CopyValueAction extends Action { constructor(id: string, label: string, private value: any, @IDebugService private debugService: IDebugService) { super(id, label, 'debug-action copy-value'); + this._enabled = typeof this.value === 'string' || (this.value instanceof Variable && !!this.value.evaluateName); } public run(): TPromise { @@ -38,15 +39,13 @@ export class CopyEvaluatePathAction extends Action { static readonly ID = 'workbench.debug.viewlet.action.copyEvaluatePath'; static LABEL = nls.localize('copyAsExpression', "Copy as Expression"); - constructor(id: string, label: string, private value: any) { + constructor(id: string, label: string, private value: Variable) { super(id, label); + this._enabled = this.value && !!this.value.evaluateName; } public run(): TPromise { - if (this.value instanceof Variable) { - clipboard.writeText(this.value.evaluateName); - } - + clipboard.writeText(this.value.evaluateName); return TPromise.as(null); } } diff --git a/src/vs/workbench/parts/debug/electron-browser/rawDebugSession.ts b/src/vs/workbench/parts/debug/electron-browser/rawDebugSession.ts index 04f3a0dfe6e..dd96720d946 100644 --- a/src/vs/workbench/parts/debug/electron-browser/rawDebugSession.ts +++ b/src/vs/workbench/parts/debug/electron-browser/rawDebugSession.ts @@ -4,27 +4,20 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; -import * as cp from 'child_process'; import * as net from 'net'; import { Event, Emitter } from 'vs/base/common/event'; -import * as platform from 'vs/base/common/platform'; import * as objects from 'vs/base/common/objects'; import { Action } from 'vs/base/common/actions'; import * as errors from 'vs/base/common/errors'; import { TPromise } from 'vs/base/common/winjs.base'; -import * as stdfork from 'vs/base/node/stdFork'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { ITerminalService } from 'vs/workbench/parts/terminal/common/terminal'; -import { ITerminalService as IExternalTerminalService } from 'vs/workbench/parts/execution/common/execution'; import * as debug from 'vs/workbench/parts/debug/common/debug'; -import { Adapter } from 'vs/workbench/parts/debug/node/debugAdapter'; -import { V8Protocol } from 'vs/workbench/parts/debug/node/v8Protocol'; +import { Debugger } from 'vs/workbench/parts/debug/node/debugger'; import { IOutputService } from 'vs/workbench/parts/output/common/output'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; -import { ExtensionsChannelId } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { TerminalSupport } from 'vs/workbench/parts/debug/electron-browser/terminalSupport'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { INotificationService } from 'vs/platform/notification/common/notification'; +import { StreamDebugAdapter } from 'vs/workbench/parts/debug/node/debugAdapter'; + export interface SessionExitedEvent extends debug.DebugEvent { body: { @@ -40,14 +33,46 @@ export interface SessionTerminatedEvent extends debug.DebugEvent { }; } -export class RawDebugSession extends V8Protocol implements debug.ISession { +export class SocketDebugAdapter extends StreamDebugAdapter { + + private socket: net.Socket; + + constructor(private host: string, private port: number) { + super(); + } + + startSession(): TPromise { + return new TPromise((c, e) => { + this.socket = net.createConnection(this.port, this.host, () => { + this.connect(this.socket, this.socket); + c(null); + }); + this.socket.on('error', (err: any) => { + e(err); + }); + this.socket.on('close', () => this._onExit.fire(0)); + }); + } + + stopSession(): TPromise { + if (this.socket !== null) { + this.socket.end(); + this.socket = undefined; + } + return void 0; + } +} + +export class RawDebugSession implements debug.ISession { + + private debugAdapter: debug.IDebugAdapter; public emittedStopped: boolean; public readyForBreakpoints: boolean; - private serverProcess: cp.ChildProcess; - private socket: net.Socket = null; - private cachedInitServer: TPromise; + //private serverProcess: cp.ChildProcess; + //private socket: net.Socket = null; + private cachedInitServerP: TPromise; private startTime: number; public disconnected: boolean; private sentPromises: TPromise[]; @@ -66,19 +91,15 @@ export class RawDebugSession extends V8Protocol implements debug.ISession { private readonly _onDidEvent: Emitter; constructor( - id: string, + private id: string, private debugServerPort: number, - private adapter: Adapter, + private _debugger: Debugger, public customTelemetryService: ITelemetryService, public root: IWorkspaceFolder, @INotificationService private notificationService: INotificationService, @ITelemetryService private telemetryService: ITelemetryService, - @IOutputService private outputService: IOutputService, - @ITerminalService private terminalService: ITerminalService, - @IExternalTerminalService private nativeTerminalService: IExternalTerminalService, - @IConfigurationService private configurationService: IConfigurationService + @IOutputService private outputService: IOutputService ) { - super(id); this.emittedStopped = false; this.readyForBreakpoints = false; this.allThreadsContinued = true; @@ -96,6 +117,10 @@ export class RawDebugSession extends V8Protocol implements debug.ISession { this._onDidEvent = new Emitter(); } + public getId(): string { + return this.id; + } + public get onDidInitialize(): Event { return this._onDidInitialize.event; } @@ -137,28 +162,49 @@ export class RawDebugSession extends V8Protocol implements debug.ISession { } private initServer(): TPromise { - if (this.cachedInitServer) { - return this.cachedInitServer; + + if (this.cachedInitServerP) { + return this.cachedInitServerP; } - const serverPromise = this.debugServerPort ? this.connectServer(this.debugServerPort) : this.startServer(); - this.cachedInitServer = serverPromise.then(() => { + const startSessionP = this.startSession(); + + this.cachedInitServerP = startSessionP.then(() => { this.startTime = new Date().getTime(); }, err => { - this.cachedInitServer = null; + this.cachedInitServerP = null; return TPromise.wrapError(err); }); - return this.cachedInitServer; + return this.cachedInitServerP; + } + + private startSession(): TPromise { + + const debugAdapterP = this.debugServerPort + ? TPromise.as(new SocketDebugAdapter('127.0.0.1', this.debugServerPort)) + : this._debugger.createDebugAdapter(this.root, this.outputService); + + return debugAdapterP.then(debugAdapter => { + + this.debugAdapter = debugAdapter; + + this.debugAdapter.onError(err => this.onDapServerError(err)); + this.debugAdapter.onEvent(event => this.onDapEvent(event)); + this.debugAdapter.onRequest(request => this.dispatchRequest(request)); + this.debugAdapter.onExit(code => this.onServerExit()); + + return this.debugAdapter.startSession(); + }); } public custom(request: string, args: any): TPromise { return this.send(request, args); } - protected send(command: string, args: any, cancelOnDisconnect = true): TPromise { + private send(command: string, args: any, cancelOnDisconnect = true): TPromise { return this.initServer().then(() => { - const promise = super.send(command, args).then(response => response, (errorResponse: DebugProtocol.ErrorResponse) => { + const promise = this.internalSend(command, args).then(response => response, (errorResponse: DebugProtocol.ErrorResponse) => { const error = errorResponse && errorResponse.body ? errorResponse.body.error : null; const errorMessage = errorResponse ? errorResponse.message : ''; const telemetryMessage = error ? debug.formatPII(error.format, true, error.variables) : errorMessage; @@ -199,8 +245,22 @@ export class RawDebugSession extends V8Protocol implements debug.ISession { }); } - protected onEvent(event: debug.DebugEvent): void { - event.sessionId = this.getId(); + private internalSend(command: string, args: any): TPromise { + let errorCallback: (error: Error) => void; + return new TPromise((completeDispatch, errorDispatch) => { + errorCallback = errorDispatch; + this.debugAdapter.sendRequest(command, args, (result: R) => { + if (result.success) { + completeDispatch(result); + } else { + errorDispatch(result); + } + }); + }, () => errorCallback(errors.canceled())); + } + + private onDapEvent(event: debug.DebugEvent): void { + event.sessionId = this.id; if (event.event === 'initialized') { this.readyForBreakpoints = true; @@ -290,6 +350,10 @@ export class RawDebugSession extends V8Protocol implements debug.ISession { return this.send('pause', args); } + public terminateThreads(args: DebugProtocol.TerminateThreadsArguments): TPromise { + return this.send('terminateThreads', args); + } + public setVariable(args: DebugProtocol.SetVariableArguments): TPromise { return this.send('setVariable', args); } @@ -317,7 +381,7 @@ export class RawDebugSession extends V8Protocol implements debug.ISession { this.sentPromises = []; }, 1000); - if ((this.serverProcess || this.socket) && !this.disconnected) { + if (this.debugAdapter && !this.disconnected) { // point of no return: from now on don't report any errors this.disconnected = true; return this.send('disconnect', { restart: restart }, false).then(() => this.stopServer(), () => this.stopServer()); @@ -392,17 +456,26 @@ export class RawDebugSession extends V8Protocol implements debug.ISession { return (new Date().getTime() - this.startTime) / 1000; } - protected dispatchRequest(request: DebugProtocol.Request, response: DebugProtocol.Response): void { + private dispatchRequest(request: DebugProtocol.Request): void { + + const response: DebugProtocol.Response = { + type: 'response', + seq: 0, + command: request.command, + request_seq: request.seq, + success: true + }; if (request.command === 'runInTerminal') { - TerminalSupport.runInTerminal(this.terminalService, this.nativeTerminalService, this.configurationService, request.arguments, response).then(() => { - this.sendResponse(response); - }, e => { + this._debugger.runInTerminal(request.arguments).then(_ => { + this.debugAdapter.sendResponse(response); + }, err => { response.success = false; - response.message = e.message; - this.sendResponse(response); + response.message = err.message; + this.debugAdapter.sendResponse(response); }); + } else if (request.command === 'handshake') { try { const vsda = require.__$__nodeRequire('vsda'); @@ -411,16 +484,16 @@ export class RawDebugSession extends V8Protocol implements debug.ISession { response.body = { signature: sig }; - this.sendResponse(response); + this.debugAdapter.sendResponse(response); } catch (e) { response.success = false; response.message = e.message; - this.sendResponse(response); + this.debugAdapter.sendResponse(response); } } else { response.success = false; response.message = `unknown request '${request.command}'`; - this.sendResponse(response); + this.debugAdapter.sendResponse(response); } } @@ -436,111 +509,36 @@ export class RawDebugSession extends V8Protocol implements debug.ISession { }); } - private connectServer(port: number): TPromise { - return new TPromise((c, e) => { - this.socket = net.createConnection(port, '127.0.0.1', () => { - this.connect(this.socket, this.socket); - c(null); - }); - this.socket.on('error', (err: any) => { - e(err); - }); - this.socket.on('close', () => this.onServerExit()); - }); - } - - private startServer(): TPromise { - return this.adapter.getAdapterExecutable(this.root).then(ae => this.launchServer(ae).then(() => { - this.serverProcess.on('error', (err: Error) => this.onServerError(err)); - this.serverProcess.on('exit', (code: number, signal: string) => this.onServerExit()); - - const sanitize = (s: string) => s.toString().replace(/\r?\n$/mg, ''); - // this.serverProcess.stdout.on('data', (data: string) => { - // console.log('%c' + sanitize(data), 'background: #ddd; font-style: italic;'); - // }); - this.serverProcess.stderr.on('data', (data: string) => { - this.outputService.getChannel(ExtensionsChannelId).append(sanitize(data)); - }); - - this.connect(this.serverProcess.stdout, this.serverProcess.stdin); - })); - } - - private launchServer(launch: debug.IAdapterExecutable): TPromise { - return new TPromise((c, e) => { - if (launch.command === 'node') { - if (Array.isArray(launch.args) && launch.args.length > 0) { - stdfork.fork(launch.args[0], launch.args.slice(1), {}, (err, child) => { - if (err) { - e(new Error(nls.localize('unableToLaunchDebugAdapter', "Unable to launch debug adapter from '{0}'.", launch.args[0]))); - } - this.serverProcess = child; - c(null); - }); - } else { - e(new Error(nls.localize('unableToLaunchDebugAdapterNoArgs', "Unable to launch debug adapter."))); - } - } else { - this.serverProcess = cp.spawn(launch.command, launch.args, { - stdio: [ - 'pipe', // stdin - 'pipe', // stdout - 'pipe' // stderr - ], - }); - c(null); - } - }); - } - private stopServer(): TPromise { - if (this.socket !== null) { - this.socket.end(); - this.cachedInitServer = null; + if (/* this.socket !== null */ this.debugAdapter instanceof SocketDebugAdapter) { + this.debugAdapter.stopSession(); + this.cachedInitServerP = null; } - this.onEvent({ event: 'exit', type: 'event', seq: 0 }); - if (!this.serverProcess) { + this.onDapEvent({ event: 'exit', type: 'event', seq: 0 }); + if (/* !this.serverProcess */ this.debugAdapter instanceof SocketDebugAdapter) { return TPromise.as(null); } this.disconnected = true; - let ret: TPromise; - // when killing a process in windows its child - // processes are *not* killed but become root - // processes. Therefore we use TASKKILL.EXE - if (platform.isWindows) { - ret = new TPromise((c, e) => { - const killer = cp.exec(`taskkill /F /T /PID ${this.serverProcess.pid}`, function (err, stdout, stderr) { - if (err) { - return e(err); - } - }); - killer.on('exit', c); - killer.on('error', e); - }); - } else { - this.serverProcess.kill('SIGTERM'); - ret = TPromise.as(null); - } - - return ret; + return this.debugAdapter.stopSession(); } - protected onServerError(err: Error): void { - this.notificationService.error(nls.localize('stoppingDebugAdapter', "{0}. Stopping the debug adapter.", err.message)); + private onDapServerError(err: Error): void { + this.notificationService.error(err.message || err.toString()); this.stopServer().done(null, errors.onUnexpectedError); } private onServerExit(): void { - this.serverProcess = null; - this.cachedInitServer = null; + //this.serverProcess = null; + this.debugAdapter = null; + this.cachedInitServerP = null; if (!this.disconnected) { this.notificationService.error(nls.localize('debugAdapterCrash', "Debug adapter process has terminated unexpectedly")); } - this.onEvent({ event: 'exit', type: 'event', seq: 0 }); + this.onDapEvent({ event: 'exit', type: 'event', seq: 0 }); } public dispose(): void { diff --git a/src/vs/workbench/parts/debug/electron-browser/repl.ts b/src/vs/workbench/parts/debug/electron-browser/repl.ts index da481600c74..f11e1016db6 100644 --- a/src/vs/workbench/parts/debug/electron-browser/repl.ts +++ b/src/vs/workbench/parts/debug/electron-browser/repl.ts @@ -10,7 +10,6 @@ import { wireCancellationToken } from 'vs/base/common/async'; import { TPromise } from 'vs/base/common/winjs.base'; import * as errors from 'vs/base/common/errors'; import { IAction } from 'vs/base/common/actions'; -import { Dimension, Builder } from 'vs/base/browser/builder'; import * as dom from 'vs/base/browser/dom'; import { isMacintosh } from 'vs/base/common/platform'; import { CancellationToken } from 'vs/base/common/cancellation'; @@ -80,7 +79,7 @@ export class Repl extends Panel implements IPrivateReplService { private replInputContainer: HTMLElement; private refreshTimeoutHandle: number; private actions: IAction[]; - private dimension: Dimension; + private dimension: dom.Dimension; private replInputHeight: number; private model: ITextModel; @@ -127,9 +126,9 @@ export class Repl extends Panel implements IPrivateReplService { } } - public create(parent: Builder): TPromise { + public create(parent: HTMLElement): TPromise { super.create(parent); - this.container = dom.append(parent.getHTMLElement(), $('.repl')); + this.container = dom.append(parent, $('.repl')); this.treeContainer = dom.append(this.container, $('.repl-tree')); this.createReplInput(this.container); @@ -242,7 +241,7 @@ export class Repl extends Panel implements IPrivateReplService { return text; } - public layout(dimension: Dimension): void { + public layout(dimension: dom.Dimension): void { this.dimension = dimension; if (this.tree) { this.renderer.setWidth(dimension.width - 25, this.characterWidth); diff --git a/src/vs/workbench/parts/debug/electron-browser/simpleDebugEditor.ts b/src/vs/workbench/parts/debug/electron-browser/simpleDebugEditor.ts index 2b7a2e33839..fd1e6e04bf5 100644 --- a/src/vs/workbench/parts/debug/electron-browser/simpleDebugEditor.ts +++ b/src/vs/workbench/parts/debug/electron-browser/simpleDebugEditor.ts @@ -19,6 +19,7 @@ import { SuggestController } from 'vs/editor/contrib/suggest/suggestController'; import { SnippetController2 } from 'vs/editor/contrib/snippet/snippetController2'; import { TabCompletionController } from 'vs/workbench/parts/snippets/electron-browser/tabCompletion'; import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { INotificationService } from 'vs/platform/notification/common/notification'; export class SimpleDebugEditor extends CodeEditorWidget { constructor( @@ -28,9 +29,10 @@ export class SimpleDebugEditor extends CodeEditorWidget { @ICodeEditorService codeEditorService: ICodeEditorService, @ICommandService commandService: ICommandService, @IContextKeyService contextKeyService: IContextKeyService, - @IThemeService themeService: IThemeService + @IThemeService themeService: IThemeService, + @INotificationService notificationService: INotificationService, ) { - super(domElement, options, true, instantiationService, codeEditorService, commandService, contextKeyService, themeService); + super(domElement, options, true, instantiationService, codeEditorService, commandService, contextKeyService, themeService, notificationService); } protected _getContributions(): IEditorContributionCtor[] { @@ -56,6 +58,7 @@ export class SimpleDebugEditor extends CodeEditorWidget { lineNumbers: 'off', folding: false, selectOnLineNumbers: false, + hideCursorInOverviewRuler: true, selectionHighlight: false, scrollbar: { horizontal: 'hidden' diff --git a/src/vs/workbench/parts/debug/electron-browser/terminalSupport.ts b/src/vs/workbench/parts/debug/electron-browser/terminalSupport.ts index 084ff3264de..d6901928791 100644 --- a/src/vs/workbench/parts/debug/electron-browser/terminalSupport.ts +++ b/src/vs/workbench/parts/debug/electron-browser/terminalSupport.ts @@ -8,41 +8,47 @@ import * as platform from 'vs/base/common/platform'; import * as cp from 'child_process'; import { IDisposable } from 'vs/base/common/lifecycle'; import { TPromise } from 'vs/base/common/winjs.base'; -import { ITerminalService, ITerminalInstance, ITerminalConfiguration } from 'vs/workbench/parts/terminal/common/terminal'; +import { ITerminalService, ITerminalInstance } from 'vs/workbench/parts/terminal/common/terminal'; import { ITerminalService as IExternalTerminalService } from 'vs/workbench/parts/execution/common/execution'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ITerminalLauncher, ITerminalSettings } from 'vs/workbench/parts/debug/common/debug'; const enum ShellType { cmd, powershell, bash } -export class TerminalSupport { +export class TerminalLauncher implements ITerminalLauncher { - private static integratedTerminalInstance: ITerminalInstance; - private static terminalDisposedListener: IDisposable; + private integratedTerminalInstance: ITerminalInstance; + private terminalDisposedListener: IDisposable; - public static runInTerminal(terminalService: ITerminalService, nativeTerminalService: IExternalTerminalService, configurationService: IConfigurationService, args: DebugProtocol.RunInTerminalRequestArguments, response: DebugProtocol.RunInTerminalResponse): TPromise { + constructor( + @ITerminalService private terminalService: ITerminalService, + @IExternalTerminalService private nativeTerminalService: IExternalTerminalService + ) { + } + + runInTerminal(args: DebugProtocol.RunInTerminalRequestArguments, config: ITerminalSettings): TPromise { if (args.kind === 'external') { - return nativeTerminalService.runInTerminal(args.title, args.cwd, args.args, args.env || {}); + return this.nativeTerminalService.runInTerminal(args.title, args.cwd, args.args, args.env || {}); } - if (!TerminalSupport.terminalDisposedListener) { + if (!this.terminalDisposedListener) { // React on terminal disposed and check if that is the debug terminal #12956 - TerminalSupport.terminalDisposedListener = terminalService.onInstanceDisposed(terminal => { - if (TerminalSupport.integratedTerminalInstance && TerminalSupport.integratedTerminalInstance.id === terminal.id) { - TerminalSupport.integratedTerminalInstance = null; + this.terminalDisposedListener = this.terminalService.onInstanceDisposed(terminal => { + if (this.integratedTerminalInstance && this.integratedTerminalInstance.id === terminal.id) { + this.integratedTerminalInstance = null; } }); } - let t = TerminalSupport.integratedTerminalInstance; + let t = this.integratedTerminalInstance; if ((t && this.isBusy(t)) || !t) { - t = terminalService.createInstance({ name: args.title || nls.localize('debug.terminal.title', "debuggee") }); - TerminalSupport.integratedTerminalInstance = t; + t = this.terminalService.createTerminal({ name: args.title || nls.localize('debug.terminal.title', "debuggee") }); + this.integratedTerminalInstance = t; } - terminalService.setActiveInstance(t); - terminalService.showPanel(true); + this.terminalService.setActiveInstance(t); + this.terminalService.showPanel(true); - const command = this.prepareCommand(args, configurationService); + const command = this.prepareCommand(args, config); return new TPromise((resolve, error) => { setTimeout(_ => { @@ -52,7 +58,7 @@ export class TerminalSupport { }); } - private static isBusy(t: ITerminalInstance): boolean { + private isBusy(t: ITerminalInstance): boolean { if (t.processId) { try { // if shell has at least one child process, assume that shell is busy @@ -82,13 +88,13 @@ export class TerminalSupport { return true; } - private static prepareCommand(args: DebugProtocol.RunInTerminalRequestArguments, configurationService: IConfigurationService): string { + private prepareCommand(args: DebugProtocol.RunInTerminalRequestArguments, config: ITerminalSettings): string { let shellType: ShellType; // get the shell configuration for the current platform let shell: string; - const shell_config = (configurationService.getValue().terminal.integrated).shell; + const shell_config = config.integrated.shell; if (platform.isWindows) { shell = shell_config.windows; shellType = ShellType.cmd; @@ -206,4 +212,4 @@ export class TerminalSupport { return command; } -} \ No newline at end of file +} diff --git a/src/vs/workbench/parts/debug/node/debugAdapter.ts b/src/vs/workbench/parts/debug/node/debugAdapter.ts index 632847907bb..c74bd1c9e4d 100644 --- a/src/vs/workbench/parts/debug/node/debugAdapter.ts +++ b/src/vs/workbench/parts/debug/node/debugAdapter.ts @@ -4,263 +4,512 @@ *--------------------------------------------------------------------------------------------*/ import * as fs from 'fs'; -import * as path from 'path'; +import * as cp from 'child_process'; +import * as stream from 'stream'; import * as nls from 'vs/nls'; -import { TPromise } from 'vs/base/common/winjs.base'; +import * as paths from 'vs/base/common/paths'; import * as strings from 'vs/base/common/strings'; import * as objects from 'vs/base/common/objects'; -import * as paths from 'vs/base/common/paths'; import * as platform from 'vs/base/common/platform'; -import { IJSONSchema, IJSONSchemaSnippet } from 'vs/base/common/jsonSchema'; -import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; -import { IConfig, IRawAdapter, IAdapterExecutable, INTERNAL_CONSOLE_OPTIONS_SCHEMA, IConfigurationManager } from 'vs/workbench/parts/debug/common/debug'; +import * as stdfork from 'vs/base/node/stdFork'; +import { Emitter, Event } from 'vs/base/common/event'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { ExtensionsChannelId } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { ICommandService } from 'vs/platform/commands/common/commands'; +import { IOutputService } from 'vs/workbench/parts/output/common/output'; +import { IDebugAdapter, IAdapterExecutable, IDebuggerContribution, IPlatformSpecificAdapterContribution, IConfig } from 'vs/workbench/parts/debug/common/debug'; +import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; +import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; -export class Adapter { +/** + * Abstract implementation of the low level API for a debug adapter. + * Missing is how this API communicates with the debug adapter. + */ +export abstract class AbstractDebugAdapter implements IDebugAdapter { - constructor(private configurationManager: IConfigurationManager, private rawAdapter: IRawAdapter, public extensionDescription: IExtensionDescription, - @IConfigurationService private configurationService: IConfigurationService, - @ICommandService private commandService: ICommandService - ) { - if (rawAdapter.windows) { - rawAdapter.win = rawAdapter.windows; + private sequence: number; + private pendingRequests: Map void>; + private requestCallback: (request: DebugProtocol.Request) => void; + private eventCallback: (request: DebugProtocol.Event) => void; + + protected readonly _onError: Emitter; + protected readonly _onExit: Emitter; + + constructor() { + this.sequence = 1; + this.pendingRequests = new Map void>(); + + this._onError = new Emitter(); + this._onExit = new Emitter(); + } + + abstract startSession(): TPromise; + abstract stopSession(): TPromise; + + public dispose(): void { + } + + abstract sendMessage(message: DebugProtocol.ProtocolMessage): void; + + public get onError(): Event { + return this._onError.event; + } + + public get onExit(): Event { + return this._onExit.event; + } + + public onEvent(callback: (event: DebugProtocol.Event) => void): void { + if (this.eventCallback) { + this._onError.fire(new Error(`attempt to set more than one 'Event' callback`)); + } + this.eventCallback = callback; + } + + public onRequest(callback: (request: DebugProtocol.Request) => void): void { + if (this.requestCallback) { + this._onError.fire(new Error(`attempt to set more than one 'Request' callback`)); + } + this.requestCallback = callback; + } + + public sendResponse(response: DebugProtocol.Response): void { + if (response.seq > 0) { + this._onError.fire(new Error(`attempt to send more than one response for command ${response.command}`)); + } else { + this.internalSend('response', response); } } - public hasConfigurationProvider = false; + public sendRequest(command: string, args: any, clb: (result: DebugProtocol.Response) => void): void { - public getAdapterExecutable(root: IWorkspaceFolder, verifyAgainstFS = true): TPromise { + const request: any = { + command: command + }; + if (args && Object.keys(args).length > 0) { + request.arguments = args; + } - return this.configurationManager.debugAdapterExecutable(root ? root.uri : undefined, this.rawAdapter.type).then(adapterExecutable => { + this.internalSend('request', request); - if (adapterExecutable) { - return this.verifyAdapterDetails(adapterExecutable, verifyAgainstFS); - } - - // try deprecated command based extension API - if (this.rawAdapter.adapterExecutableCommand) { - return this.commandService.executeCommand(this.rawAdapter.adapterExecutableCommand, root ? root.uri.toString() : undefined).then(ad => { - return this.verifyAdapterDetails(ad, verifyAgainstFS); - }); - } - - // fallback: executable contribution specified in package.json - adapterExecutable = { - command: this.getProgram(), - args: this.getAttributeBasedOnPlatform('args') - }; - const runtime = this.getRuntime(); - if (runtime) { - const runtimeArgs = this.getAttributeBasedOnPlatform('runtimeArgs'); - adapterExecutable.args = (runtimeArgs || []).concat([adapterExecutable.command]).concat(adapterExecutable.args || []); - adapterExecutable.command = runtime; - } - return this.verifyAdapterDetails(adapterExecutable, verifyAgainstFS); - }); + if (clb) { + // store callback for this request + this.pendingRequests.set(request.seq, clb); + } } - private verifyAdapterDetails(details: IAdapterExecutable, verifyAgainstFS: boolean): TPromise { + public acceptMessage(message: DebugProtocol.ProtocolMessage): void { + switch (message.type) { + case 'event': + if (this.eventCallback) { + this.eventCallback(message); + } + break; + case 'request': + if (this.requestCallback) { + this.requestCallback(message); + } + break; + case 'response': + const response = message; + const clb = this.pendingRequests.get(response.request_seq); + if (clb) { + this.pendingRequests.delete(response.request_seq); + clb(response); + } + break; + } + } - if (details.command) { - if (verifyAgainstFS) { - if (path.isAbsolute(details.command)) { - return new TPromise((c, e) => { - fs.exists(details.command, exists => { - if (exists) { - c(details); - } else { - e(new Error(nls.localize('debugAdapterBinNotFound', "Debug adapter executable '{0}' does not exist.", details.command))); - } - }); - }); + private internalSend(typ: 'request' | 'response' | 'event', message: DebugProtocol.ProtocolMessage): void { + + message.type = typ; + message.seq = this.sequence++; + + this.sendMessage(message); + } +} + +/** + * An implementation that communicates via two streams with the debug adapter. + */ +export abstract class StreamDebugAdapter extends AbstractDebugAdapter { + + private static readonly TWO_CRLF = '\r\n\r\n'; + private static readonly HEADER_LINESEPARATOR = /\r?\n/; // allow for non-RFC 2822 conforming line separators + private static readonly HEADER_FIELDSEPARATOR = /: */; + + private outputStream: stream.Writable; + private rawData: Buffer; + private contentLength: number; + + constructor() { + super(); + } + + public connect(readable: stream.Readable, writable: stream.Writable): void { + + this.outputStream = writable; + this.rawData = Buffer.allocUnsafe(0); + this.contentLength = -1; + + readable.on('data', (data: Buffer) => this.handleData(data)); + + // readable.on('close', () => { + // this._emitEvent(new Event('close')); + // }); + // readable.on('error', (error) => { + // this._emitEvent(new Event('error', 'readable error: ' + (error && error.message))); + // }); + + // writable.on('error', (error) => { + // this._emitEvent(new Event('error', 'writable error: ' + (error && error.message))); + // }); + } + + public sendMessage(message: DebugProtocol.ProtocolMessage): void { + + if (this.outputStream) { + const json = JSON.stringify(message); + this.outputStream.write(`Content-Length: ${Buffer.byteLength(json, 'utf8')}${StreamDebugAdapter.TWO_CRLF}${json}`, 'utf8'); + } + } + + private handleData(data: Buffer): void { + + this.rawData = Buffer.concat([this.rawData, data]); + + while (true) { + if (this.contentLength >= 0) { + if (this.rawData.length >= this.contentLength) { + const message = this.rawData.toString('utf8', 0, this.contentLength); + this.rawData = this.rawData.slice(this.contentLength); + this.contentLength = -1; + if (message.length > 0) { + try { + this.acceptMessage(JSON.parse(message)); + } catch (e) { + this._onError.fire(new Error((e.message || e) + '\n' + message)); + } + } + continue; // there may be more complete messages to process + } + } else { + const idx = this.rawData.indexOf(StreamDebugAdapter.TWO_CRLF); + if (idx !== -1) { + const header = this.rawData.toString('utf8', 0, idx); + const lines = header.split(StreamDebugAdapter.HEADER_LINESEPARATOR); + for (const h of lines) { + const kvPair = h.split(StreamDebugAdapter.HEADER_FIELDSEPARATOR); + if (kvPair[0] === 'Content-Length') { + this.contentLength = Number(kvPair[1]); + } + } + this.rawData = this.rawData.slice(idx + StreamDebugAdapter.TWO_CRLF.length); + continue; + } + } + break; + } + } +} + +/** + * An implementation that launches the debug adapter as a separate process and communicates via stdin/stdout. +*/ +export class DebugAdapter extends StreamDebugAdapter { + + private _serverProcess: cp.ChildProcess; + + constructor(private _debugType: string, private _adapterExecutable: IAdapterExecutable | null, extensionDescriptions: IExtensionDescription[], private _outputService?: IOutputService) { + super(); + + if (!this._adapterExecutable) { + this._adapterExecutable = DebugAdapter.platformAdapterExecutable(extensionDescriptions, this._debugType); + } + } + + startSession(): TPromise { + + return new TPromise((c, e) => { + + // verify executables + if (this._adapterExecutable.command) { + if (paths.isAbsolute(this._adapterExecutable.command)) { + if (!fs.existsSync(this._adapterExecutable.command)) { + e(new Error(nls.localize('debugAdapterBinNotFound', "Debug adapter executable '{0}' does not exist.", this._adapterExecutable.command))); + } } else { // relative path - if (details.command.indexOf('/') < 0 && details.command.indexOf('\\') < 0) { + if (this._adapterExecutable.command.indexOf('/') < 0 && this._adapterExecutable.command.indexOf('\\') < 0) { // no separators: command looks like a runtime name like 'node' or 'mono' - return TPromise.as(details); // TODO: check that the runtime is available on PATH + // TODO: check that the runtime is available on PATH } } } else { - return TPromise.as(details); + e(new Error(nls.localize({ key: 'debugAdapterCannotDetermineExecutable', comment: ['Adapter executable file not found'] }, + "Cannot determine executable for debug adapter '{0}'.", this._debugType))); } - } - return TPromise.wrapError(new Error(nls.localize({ key: 'debugAdapterCannotDetermineExecutable', comment: ['Adapter executable file not found'] }, - "Cannot determine executable for debug adapter '{0}'.", this.type))); - } - - private getRuntime(): string { - let runtime = this.getAttributeBasedOnPlatform('runtime'); - if (runtime && runtime.indexOf('./') === 0) { - runtime = paths.join(this.extensionDescription.extensionFolderPath, runtime); - } - return runtime; - } - - private getProgram(): string { - let program = this.getAttributeBasedOnPlatform('program'); - if (program) { - program = paths.join(this.extensionDescription.extensionFolderPath, program); - } - return program; - } - - public get aiKey(): string { - return this.rawAdapter.aiKey; - } - - public get label(): string { - return this.rawAdapter.label || this.rawAdapter.type; - } - - public get type(): string { - return this.rawAdapter.type; - } - - public get variables(): { [key: string]: string } { - return this.rawAdapter.variables; - } - - public get configurationSnippets(): IJSONSchemaSnippet[] { - return this.rawAdapter.configurationSnippets; - } - - public get languages(): string[] { - return this.rawAdapter.languages; - } - - public merge(secondRawAdapter: IRawAdapter, extensionDescription: IExtensionDescription): void { - // Give priority to built in debug adapters - if (extensionDescription.isBuiltin) { - this.extensionDescription = extensionDescription; - } - objects.mixin(this.rawAdapter, secondRawAdapter, extensionDescription.isBuiltin); - } - - public hasInitialConfiguration(): boolean { - return !!this.rawAdapter.initialConfigurations; - } - - public getInitialConfigurationContent(initialConfigs?: IConfig[]): TPromise { - // at this point we got some configs from the package.json and/or from registered DebugConfigurationProviders - let initialConfigurations = this.rawAdapter.initialConfigurations || []; - if (initialConfigs) { - initialConfigurations = initialConfigurations.concat(initialConfigs); - } - - const configs = JSON.stringify(initialConfigurations, null, '\t').split('\n').map(line => '\t' + line).join('\n').trim(); - const comment1 = nls.localize('launch.config.comment1', "Use IntelliSense to learn about possible attributes."); - const comment2 = nls.localize('launch.config.comment2', "Hover to view descriptions of existing attributes."); - const comment3 = nls.localize('launch.config.comment3', "For more information, visit: {0}", 'https://go.microsoft.com/fwlink/?linkid=830387'); - - let content = [ - '{', - `\t// ${comment1}`, - `\t// ${comment2}`, - `\t// ${comment3}`, - `\t"version": "0.2.0",`, - `\t"configurations": ${configs}`, - '}' - ].join('\n'); - - // fix formatting - const editorConfig = this.configurationService.getValue(); - if (editorConfig.editor && editorConfig.editor.insertSpaces) { - content = content.replace(new RegExp('\t', 'g'), strings.repeat(' ', editorConfig.editor.tabSize)); - } - - return TPromise.as(content); - } - - public getSchemaAttributes(): IJSONSchema[] { - if (!this.rawAdapter.configurationAttributes) { - return null; - } - // fill in the default configuration attributes shared by all adapters. - return Object.keys(this.rawAdapter.configurationAttributes).map(request => { - const attributes: IJSONSchema = this.rawAdapter.configurationAttributes[request]; - const defaultRequired = ['name', 'type', 'request']; - attributes.required = attributes.required && attributes.required.length ? defaultRequired.concat(attributes.required) : defaultRequired; - attributes.additionalProperties = false; - attributes.type = 'object'; - if (!attributes.properties) { - attributes.properties = {}; + if (this._adapterExecutable.command === 'node' && this._outputService) { + if (Array.isArray(this._adapterExecutable.args) && this._adapterExecutable.args.length > 0) { + stdfork.fork(this._adapterExecutable.args[0], this._adapterExecutable.args.slice(1), {}, (err, child) => { + if (err) { + e(new Error(nls.localize('unableToLaunchDebugAdapter', "Unable to launch debug adapter from '{0}'.", this._adapterExecutable.args[0]))); + } + this._serverProcess = child; + c(null); + }); + } else { + e(new Error(nls.localize('unableToLaunchDebugAdapterNoArgs', "Unable to launch debug adapter."))); + } + } else { + this._serverProcess = cp.spawn(this._adapterExecutable.command, this._adapterExecutable.args); + c(null); } - const properties = attributes.properties; - properties['type'] = { - enum: [this.type], - description: nls.localize('debugType', "Type of configuration."), - pattern: '^(?!node2)', - errorMessage: nls.localize('debugTypeNotRecognised', "The debug type is not recognized. Make sure that you have a corresponding debug extension installed and that it is enabled."), - patternErrorMessage: nls.localize('node2NotSupported', "\"node2\" is no longer supported, use \"node\" instead and set the \"protocol\" attribute to \"inspector\".") - }; - properties['name'] = { - type: 'string', - description: nls.localize('debugName', "Name of configuration; appears in the launch configuration drop down menu."), - default: 'Launch' - }; - properties['request'] = { - enum: [request], - description: nls.localize('debugRequest', "Request type of configuration. Can be \"launch\" or \"attach\"."), - }; - properties['debugServer'] = { - type: 'number', - description: nls.localize('debugServer', "For debug extension development only: if a port is specified VS Code tries to connect to a debug adapter running in server mode"), - default: 4711 - }; - properties['preLaunchTask'] = { - type: ['string', 'null'], - default: null, - description: nls.localize('debugPrelaunchTask', "Task to run before debug session starts.") - }; - properties['postDebugTask'] = { - type: ['string', 'null'], - default: null, - description: nls.localize('debugPostDebugTask', "Task to run after debug session ends.") - }; - properties['internalConsoleOptions'] = INTERNAL_CONSOLE_OPTIONS_SCHEMA; + }).then(_ => { + this._serverProcess.on('error', (err: Error) => this._onError.fire(err)); + this._serverProcess.on('exit', (code: number, signal: string) => this._onExit.fire(code)); - const osProperties = objects.deepClone(properties); - properties['windows'] = { - type: 'object', - description: nls.localize('debugWindowsConfiguration', "Windows specific launch configuration attributes."), - properties: osProperties - }; - properties['osx'] = { - type: 'object', - description: nls.localize('debugOSXConfiguration', "OS X specific launch configuration attributes."), - properties: osProperties - }; - properties['linux'] = { - type: 'object', - description: nls.localize('debugLinuxConfiguration', "Linux specific launch configuration attributes."), - properties: osProperties - }; - Object.keys(attributes.properties).forEach(name => { - // Use schema allOf property to get independent error reporting #21113 - attributes.properties[name].pattern = attributes.properties[name].pattern || '^(?!.*\\$\\{(env|config|command)\\.)'; - attributes.properties[name].patternErrorMessage = attributes.properties[name].patternErrorMessage || - nls.localize('deprecatedVariables', "'env.', 'config.' and 'command.' are deprecated, use 'env:', 'config:' and 'command:' instead."); - }); + if (this._outputService) { + const sanitize = (s: string) => s.toString().replace(/\r?\n$/mg, ''); + // this.serverProcess.stdout.on('data', (data: string) => { + // console.log('%c' + sanitize(data), 'background: #ddd; font-style: italic;'); + // }); + this._serverProcess.stderr.on('data', (data: string) => { + this._outputService.getChannel(ExtensionsChannelId).append(sanitize(data)); + }); + } - return attributes; + this.connect(this._serverProcess.stdout, this._serverProcess.stdin); + }, err => { + this._onError.fire(err); }); } - private getAttributeBasedOnPlatform(key: string): any { - let result: any; - if (platform.isWindows && !process.env.hasOwnProperty('PROCESSOR_ARCHITEW6432') && this.rawAdapter.winx86) { - result = this.rawAdapter.winx86[key]; - } else if (platform.isWindows && this.rawAdapter.win) { - result = this.rawAdapter.win[key]; - } else if (platform.isMacintosh && this.rawAdapter.osx) { - result = this.rawAdapter.osx[key]; - } else if (platform.isLinux && this.rawAdapter.linux) { - result = this.rawAdapter.linux[key]; + stopSession(): TPromise { + + // when killing a process in windows its child + // processes are *not* killed but become root + // processes. Therefore we use TASKKILL.EXE + if (platform.isWindows) { + return new TPromise((c, e) => { + const killer = cp.exec(`taskkill /F /T /PID ${this._serverProcess.pid}`, function (err, stdout, stderr) { + if (err) { + return e(err); + } + }); + killer.on('exit', c); + killer.on('error', e); + }); + } else { + this._serverProcess.kill('SIGTERM'); + return TPromise.as(null); + } + } + + private static extract(contribution: IDebuggerContribution, extensionFolderPath: string): IDebuggerContribution { + if (!contribution) { + return undefined; + } + let result: IDebuggerContribution = {}; + + if (contribution.runtime) { + if (contribution.runtime.indexOf('./') === 0) { // TODO + result.runtime = paths.join(extensionFolderPath, contribution.runtime); + } else { + result.runtime = contribution.runtime; + } + } + if (contribution.runtimeArgs) { + result.runtimeArgs = contribution.runtimeArgs; + } + if (contribution.program) { + if (!paths.isAbsolute(contribution.program)) { + result.program = paths.join(extensionFolderPath, contribution.program); + } else { + result.program = contribution.program; + } + } + if (contribution.args) { + result.args = contribution.args; } - return result || this.rawAdapter[key]; + if (contribution.win) { + result.win = DebugAdapter.extract(contribution.win, extensionFolderPath); + } + if (contribution.winx86) { + result.winx86 = DebugAdapter.extract(contribution.winx86, extensionFolderPath); + } + if (contribution.windows) { + result.windows = DebugAdapter.extract(contribution.windows, extensionFolderPath); + } + if (contribution.osx) { + result.osx = DebugAdapter.extract(contribution.osx, extensionFolderPath); + } + if (contribution.linux) { + result.linux = DebugAdapter.extract(contribution.linux, extensionFolderPath); + } + return result; + } + + static platformAdapterExecutable(extensionDescriptions: IExtensionDescription[], debugType: string): IAdapterExecutable { + + let result: IDebuggerContribution = {}; + + debugType = debugType.toLowerCase(); + + // merge all contributions into one + for (const ed of extensionDescriptions) { + if (ed.contributes) { + const debuggers = ed.contributes['debuggers']; + if (debuggers && debuggers.length > 0) { + const dbgs = debuggers.filter(d => strings.equalsIgnoreCase(d.type, debugType)); + for (const dbg of dbgs) { + + // extract relevant attributes and make then absolute where needed + const dbg1 = DebugAdapter.extract(dbg, ed.extensionFolderPath); + + // merge + objects.mixin(result, dbg1, ed.isBuiltin); + } + } + } + } + + // select the right platform + let platformInfo: IPlatformSpecificAdapterContribution; + if (platform.isWindows && !process.env.hasOwnProperty('PROCESSOR_ARCHITEW6432')) { + platformInfo = result.winx86 || result.win || result.windows; + } else if (platform.isWindows) { + platformInfo = result.win || result.windows; + } else if (platform.isMacintosh) { + platformInfo = result.osx; + } else if (platform.isLinux) { + platformInfo = result.linux; + } + platformInfo = platformInfo || result; + + // these are the relevant attributes + let program = platformInfo.program || result.program; + const args = platformInfo.args || result.args; + let runtime = platformInfo.runtime || result.runtime; + const runtimeArgs = platformInfo.runtimeArgs || result.runtimeArgs; + + if (runtime) { + return { + command: runtime, + args: (runtimeArgs || []).concat([program]).concat(args || []) + }; + } else { + return { + command: program, + args: args || [] + }; + } + } + + static substituteVariables(workspaceFolder: IWorkspaceFolder, config: IConfig, resolverService: IConfigurationResolverService): IConfig { + + const result = objects.deepClone(config) as IConfig; + + // hoist platform specific attributes to top level + if (platform.isWindows && result.windows) { + Object.keys(result.windows).forEach(key => result[key] = result.windows[key]); + } else if (platform.isMacintosh && result.osx) { + Object.keys(result.osx).forEach(key => result[key] = result.osx[key]); + } else if (platform.isLinux && result.linux) { + Object.keys(result.linux).forEach(key => result[key] = result.linux[key]); + } + + // delete all platform specific sections + delete result.windows; + delete result.osx; + delete result.linux; + + // substitute all variables in string values + return resolverService.resolveAny(workspaceFolder, result); + } +} + +// path hooks helpers + +export function convertToDAPaths(msg: DebugProtocol.ProtocolMessage, fixSourcePaths: (source: DebugProtocol.Source) => void): void { + convertPaths(msg, (toDA: boolean, source: DebugProtocol.Source | undefined) => { + if (toDA && source) { + fixSourcePaths(source); + } + }); +} + +export function convertToVSCPaths(msg: DebugProtocol.ProtocolMessage, fixSourcePaths: (source: DebugProtocol.Source) => void): void { + convertPaths(msg, (toDA: boolean, source: DebugProtocol.Source | undefined) => { + if (!toDA && source) { + fixSourcePaths(source); + } + }); +} + +function convertPaths(msg: DebugProtocol.ProtocolMessage, fixSourcePaths: (toDA: boolean, source: DebugProtocol.Source | undefined) => void): void { + switch (msg.type) { + case 'event': + const event = msg; + switch (event.event) { + case 'output': + fixSourcePaths(false, (event).body.source); + break; + case 'loadedSource': + fixSourcePaths(false, (event).body.source); + break; + case 'breakpoint': + fixSourcePaths(false, (event).body.breakpoint.source); + break; + default: + break; + } + break; + case 'request': + const request = msg; + switch (request.command) { + case 'setBreakpoints': + fixSourcePaths(true, (request.arguments).source); + break; + case 'source': + fixSourcePaths(true, (request.arguments).source); + break; + case 'gotoTargets': + fixSourcePaths(true, (request.arguments).source); + break; + default: + break; + } + break; + case 'response': + const response = msg; + switch (response.command) { + case 'stackTrace': + const r1 = response; + r1.body.stackFrames.forEach(frame => fixSourcePaths(false, frame.source)); + break; + case 'loadedSources': + const r2 = response; + r2.body.sources.forEach(source => fixSourcePaths(false, source)); + break; + case 'scopes': + const r3 = response; + r3.body.scopes.forEach(scope => fixSourcePaths(false, scope.source)); + break; + case 'setFunctionBreakpoints': + const r4 = response; + r4.body.breakpoints.forEach(bp => fixSourcePaths(false, bp.source)); + break; + case 'setBreakpoints': + const r5 = response; + r5.body.breakpoints.forEach(bp => fixSourcePaths(false, bp.source)); + break; + default: + break; + } + break; } } diff --git a/src/vs/workbench/parts/debug/node/debugger.ts b/src/vs/workbench/parts/debug/node/debugger.ts new file mode 100644 index 00000000000..e6e1ffed7ab --- /dev/null +++ b/src/vs/workbench/parts/debug/node/debugger.ts @@ -0,0 +1,236 @@ +/*--------------------------------------------------------------------------------------------- + * 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 * as strings from 'vs/base/common/strings'; +import * as objects from 'vs/base/common/objects'; +import { IJSONSchema, IJSONSchemaSnippet } from 'vs/base/common/jsonSchema'; +import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; +import { IConfig, IDebuggerContribution, IAdapterExecutable, INTERNAL_CONSOLE_OPTIONS_SCHEMA, IConfigurationManager, IDebugAdapter, IDebugConfiguration, ITerminalSettings } from 'vs/workbench/parts/debug/common/debug'; +import { IExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { IOutputService } from 'vs/workbench/parts/output/common/output'; +import { DebugAdapter } from 'vs/workbench/parts/debug/node/debugAdapter'; +import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; + +export class Debugger { + + private _mergedExtensionDescriptions: IExtensionDescription[]; + + constructor(private configurationManager: IConfigurationManager, private debuggerContribution: IDebuggerContribution, public extensionDescription: IExtensionDescription, + @IConfigurationService private configurationService: IConfigurationService, + @ICommandService private commandService: ICommandService, + @IConfigurationResolverService private configurationResolverService: IConfigurationResolverService, + ) { + this._mergedExtensionDescriptions = [extensionDescription]; + } + + public hasConfigurationProvider = false; + + public createDebugAdapter(root: IWorkspaceFolder, outputService: IOutputService): TPromise { + return this.getAdapterExecutable(root).then(adapterExecutable => { + const debugConfigs = this.configurationService.getValue('debug'); + if (debugConfigs.extensionHostDebugAdapter) { + return this.configurationManager.createDebugAdapter(this.type, adapterExecutable); + } else { + return new DebugAdapter(this.type, adapterExecutable, this._mergedExtensionDescriptions, outputService); + } + }); + } + + public getAdapterExecutable(root: IWorkspaceFolder): TPromise { + + // first try to get an executable from DebugConfigurationProvider + return this.configurationManager.debugAdapterExecutable(root ? root.uri : undefined, this.type).then(adapterExecutable => { + + if (adapterExecutable) { + return adapterExecutable; + } + + // try deprecated command based extension API to receive an executable + if (this.debuggerContribution.adapterExecutableCommand) { + return this.commandService.executeCommand(this.debuggerContribution.adapterExecutableCommand, root ? root.uri.toString() : undefined); + } + + // give up and let DebugAdapter determine executable based on package.json contribution + return TPromise.as(null); + }); + } + + public substituteVariables(folder: IWorkspaceFolder, config: IConfig): TPromise { + + let configP: TPromise; + const debugConfigs = this.configurationService.getValue('debug'); + if (debugConfigs.extensionHostDebugAdapter) { + configP = this.configurationManager.substituteVariables(this.type, folder, config); + } else { + try { + configP = TPromise.as(DebugAdapter.substituteVariables(folder, config, this.configurationResolverService)); + } catch (e) { + return TPromise.wrapError(e); + } + } + + return configP.then(result => { + // substitute 'command' variables (including interactive) + return this.configurationResolverService.resolveInteractiveVariables(result, this.variables); + }); + } + + public runInTerminal(args: DebugProtocol.RunInTerminalRequestArguments): TPromise { + const debugConfigs = this.configurationService.getValue('debug'); + const config = this.configurationService.getValue('terminal'); + const type = debugConfigs.extensionHostDebugAdapter ? this.type : '*'; + return this.configurationManager.runInTerminal(type, args, config); + } + + public get aiKey(): string { + return this.debuggerContribution.aiKey; + } + + public get label(): string { + return this.debuggerContribution.label || this.debuggerContribution.type; + } + + public get type(): string { + return this.debuggerContribution.type; + } + + public get variables(): { [key: string]: string } { + return this.debuggerContribution.variables; + } + + public get configurationSnippets(): IJSONSchemaSnippet[] { + return this.debuggerContribution.configurationSnippets; + } + + public get languages(): string[] { + return this.debuggerContribution.languages; + } + + public merge(secondRawAdapter: IDebuggerContribution, extensionDescription: IExtensionDescription): void { + + // remember all ext descriptions that are the source of this debugger + this._mergedExtensionDescriptions.push(extensionDescription); + + // Give priority to built in debug adapters + if (extensionDescription.isBuiltin) { + this.extensionDescription = extensionDescription; + } + objects.mixin(this.debuggerContribution, secondRawAdapter, extensionDescription.isBuiltin); + } + + public hasInitialConfiguration(): boolean { + return !!this.debuggerContribution.initialConfigurations; + } + + public getInitialConfigurationContent(initialConfigs?: IConfig[]): TPromise { + // at this point we got some configs from the package.json and/or from registered DebugConfigurationProviders + let initialConfigurations = this.debuggerContribution.initialConfigurations || []; + if (initialConfigs) { + initialConfigurations = initialConfigurations.concat(initialConfigs); + } + + const configs = JSON.stringify(initialConfigurations, null, '\t').split('\n').map(line => '\t' + line).join('\n').trim(); + const comment1 = nls.localize('launch.config.comment1', "Use IntelliSense to learn about possible attributes."); + const comment2 = nls.localize('launch.config.comment2', "Hover to view descriptions of existing attributes."); + const comment3 = nls.localize('launch.config.comment3', "For more information, visit: {0}", 'https://go.microsoft.com/fwlink/?linkid=830387'); + + let content = [ + '{', + `\t// ${comment1}`, + `\t// ${comment2}`, + `\t// ${comment3}`, + `\t"version": "0.2.0",`, + `\t"configurations": ${configs}`, + '}' + ].join('\n'); + + // fix formatting + const editorConfig = this.configurationService.getValue(); + if (editorConfig.editor && editorConfig.editor.insertSpaces) { + content = content.replace(new RegExp('\t', 'g'), strings.repeat(' ', editorConfig.editor.tabSize)); + } + + return TPromise.as(content); + } + + public getSchemaAttributes(): IJSONSchema[] { + if (!this.debuggerContribution.configurationAttributes) { + return null; + } + // fill in the default configuration attributes shared by all adapters. + return Object.keys(this.debuggerContribution.configurationAttributes).map(request => { + const attributes: IJSONSchema = this.debuggerContribution.configurationAttributes[request]; + const defaultRequired = ['name', 'type', 'request']; + attributes.required = attributes.required && attributes.required.length ? defaultRequired.concat(attributes.required) : defaultRequired; + attributes.additionalProperties = false; + attributes.type = 'object'; + if (!attributes.properties) { + attributes.properties = {}; + } + const properties = attributes.properties; + properties['type'] = { + enum: [this.type], + description: nls.localize('debugType', "Type of configuration."), + pattern: '^(?!node2)', + errorMessage: nls.localize('debugTypeNotRecognised', "The debug type is not recognized. Make sure that you have a corresponding debug extension installed and that it is enabled."), + patternErrorMessage: nls.localize('node2NotSupported', "\"node2\" is no longer supported, use \"node\" instead and set the \"protocol\" attribute to \"inspector\".") + }; + properties['name'] = { + type: 'string', + description: nls.localize('debugName', "Name of configuration; appears in the launch configuration drop down menu."), + default: 'Launch' + }; + properties['request'] = { + enum: [request], + description: nls.localize('debugRequest', "Request type of configuration. Can be \"launch\" or \"attach\"."), + }; + properties['debugServer'] = { + type: 'number', + description: nls.localize('debugServer', "For debug extension development only: if a port is specified VS Code tries to connect to a debug adapter running in server mode"), + default: 4711 + }; + properties['preLaunchTask'] = { + type: ['string', 'null'], + default: '', + description: nls.localize('debugPrelaunchTask', "Task to run before debug session starts.") + }; + properties['postDebugTask'] = { + type: ['string', 'null'], + default: '', + description: nls.localize('debugPostDebugTask', "Task to run after debug session ends.") + }; + properties['internalConsoleOptions'] = INTERNAL_CONSOLE_OPTIONS_SCHEMA; + + const osProperties = objects.deepClone(properties); + properties['windows'] = { + type: 'object', + description: nls.localize('debugWindowsConfiguration', "Windows specific launch configuration attributes."), + properties: osProperties + }; + properties['osx'] = { + type: 'object', + description: nls.localize('debugOSXConfiguration', "OS X specific launch configuration attributes."), + properties: osProperties + }; + properties['linux'] = { + type: 'object', + description: nls.localize('debugLinuxConfiguration', "Linux specific launch configuration attributes."), + properties: osProperties + }; + Object.keys(attributes.properties).forEach(name => { + // Use schema allOf property to get independent error reporting #21113 + attributes.properties[name].pattern = attributes.properties[name].pattern || '^(?!.*\\$\\{(env|config|command)\\.)'; + attributes.properties[name].patternErrorMessage = attributes.properties[name].patternErrorMessage || + nls.localize('deprecatedVariables', "'env.', 'config.' and 'command.' are deprecated, use 'env:', 'config:' and 'command:' instead."); + }); + + return attributes; + }); + } +} diff --git a/src/vs/workbench/parts/debug/node/terminals.ts b/src/vs/workbench/parts/debug/node/terminals.ts new file mode 100644 index 00000000000..c0cfca5db35 --- /dev/null +++ b/src/vs/workbench/parts/debug/node/terminals.ts @@ -0,0 +1,260 @@ +/*--------------------------------------------------------------------------------------------- + * 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 cp from 'child_process'; +import * as nls from 'vs/nls'; +import * as env from 'vs/base/common/platform'; +import * as pfs from 'vs/base/node/pfs'; +import { assign } from 'vs/base/common/objects'; +import { TPromise } from 'vs/base/common/winjs.base'; +import uri from 'vs/base/common/uri'; +import { ITerminalLauncher, ITerminalSettings } from 'vs/workbench/parts/debug/common/debug'; + +const TERMINAL_TITLE = nls.localize('console.title', "VS Code Console"); + +let terminalLauncher: ITerminalLauncher = undefined; + +export function getTerminalLauncher() { + if (!terminalLauncher) { + if (env.isWindows) { + terminalLauncher = new WinTerminalService(); + } else if (env.isMacintosh) { + terminalLauncher = new MacTerminalService(); + } else if (env.isLinux) { + terminalLauncher = new LinuxTerminalService(); + } + } + return terminalLauncher; +} + +let _DEFAULT_TERMINAL_LINUX_READY: TPromise = null; +export function getDefaultTerminalLinuxReady(): TPromise { + if (!_DEFAULT_TERMINAL_LINUX_READY) { + _DEFAULT_TERMINAL_LINUX_READY = new TPromise(c => { + if (env.isLinux) { + TPromise.join([pfs.exists('/etc/debian_version'), process.lazyEnv]).then(([isDebian]) => { + if (isDebian) { + c('x-terminal-emulator'); + } else if (process.env.DESKTOP_SESSION === 'gnome' || process.env.DESKTOP_SESSION === 'gnome-classic') { + c('gnome-terminal'); + } else if (process.env.DESKTOP_SESSION === 'kde-plasma') { + c('konsole'); + } else if (process.env.COLORTERM) { + c(process.env.COLORTERM); + } else if (process.env.TERM) { + c(process.env.TERM); + } else { + c('xterm'); + } + }); + return; + } + + c('xterm'); + }, () => { }); + } + return _DEFAULT_TERMINAL_LINUX_READY; +} + +let _DEFAULT_TERMINAL_WINDOWS: string = null; +export function getDefaultTerminalWindows(): string { + if (!_DEFAULT_TERMINAL_WINDOWS) { + const isWoW64 = !!process.env.hasOwnProperty('PROCESSOR_ARCHITEW6432'); + _DEFAULT_TERMINAL_WINDOWS = `${process.env.windir ? process.env.windir : 'C:\\Windows'}\\${isWoW64 ? 'Sysnative' : 'System32'}\\cmd.exe`; + } + return _DEFAULT_TERMINAL_WINDOWS; +} + +abstract class TerminalLauncher implements ITerminalLauncher { + public runInTerminal(args: DebugProtocol.RunInTerminalRequestArguments, config: ITerminalSettings): TPromise { + return this.runInTerminal0(args.title, args.cwd, args.args, args.env || {}, config); + } + runInTerminal0(title: string, dir: string, args: string[], envVars: env.IProcessEnvironment, config): TPromise { + return void 0; + } +} + +class WinTerminalService extends TerminalLauncher { + + private static readonly CMD = 'cmd.exe'; + + public runInTerminal0(title: string, dir: string, args: string[], envVars: env.IProcessEnvironment, configuration: ITerminalSettings): TPromise { + + const exec = configuration.external.windowsExec || getDefaultTerminalWindows(); + + return new TPromise((c, e) => { + + const title = `"${dir} - ${TERMINAL_TITLE}"`; + const command = `""${args.join('" "')}" & pause"`; // use '|' to only pause on non-zero exit code + + const cmdArgs = [ + '/c', 'start', title, '/wait', exec, '/c', command + ]; + + // merge environment variables into a copy of the process.env + const env = assign({}, process.env, envVars); + + // delete environment variables that have a null value + Object.keys(env).filter(v => env[v] === null).forEach(key => delete env[key]); + + const options: any = { + cwd: dir, + env: env, + windowsVerbatimArguments: true + }; + + const cmd = cp.spawn(WinTerminalService.CMD, cmdArgs, options); + cmd.on('error', e); + + c(null); + }); + } +} + +class MacTerminalService extends TerminalLauncher { + + private static readonly DEFAULT_TERMINAL_OSX = 'Terminal.app'; + private static readonly OSASCRIPT = '/usr/bin/osascript'; // osascript is the AppleScript interpreter on OS X + + public runInTerminal0(title: string, dir: string, args: string[], envVars: env.IProcessEnvironment, configuration: ITerminalSettings): TPromise { + + const terminalApp = configuration.external.osxExec || MacTerminalService.DEFAULT_TERMINAL_OSX; + + return new TPromise((c, e) => { + + if (terminalApp === MacTerminalService.DEFAULT_TERMINAL_OSX || terminalApp === 'iTerm.app') { + + // On OS X we launch an AppleScript that creates (or reuses) a Terminal window + // and then launches the program inside that window. + + const script = terminalApp === MacTerminalService.DEFAULT_TERMINAL_OSX ? 'TerminalHelper' : 'iTermHelper'; + const scriptpath = uri.parse(require.toUrl(`vs/workbench/parts/execution/electron-browser/${script}.scpt`)).fsPath; + + const osaArgs = [ + scriptpath, + '-t', title || TERMINAL_TITLE, + '-w', dir, + ]; + + for (let a of args) { + osaArgs.push('-a'); + osaArgs.push(a); + } + + if (envVars) { + for (let key in envVars) { + const value = envVars[key]; + if (value === null) { + osaArgs.push('-u'); + osaArgs.push(key); + } else { + osaArgs.push('-e'); + osaArgs.push(`${key}=${value}`); + } + } + } + + let stderr = ''; + const osa = cp.spawn(MacTerminalService.OSASCRIPT, osaArgs); + osa.on('error', e); + osa.stderr.on('data', (data) => { + stderr += data.toString(); + }); + osa.on('exit', (code: number) => { + if (code === 0) { // OK + c(null); + } else { + if (stderr) { + const lines = stderr.split('\n', 1); + e(new Error(lines[0])); + } else { + e(new Error(nls.localize('mac.terminal.script.failed', "Script '{0}' failed with exit code {1}", script, code))); + } + } + }); + } else { + e(new Error(nls.localize('mac.terminal.type.not.supported', "'{0}' not supported", terminalApp))); + } + }); + } +} + +class LinuxTerminalService extends TerminalLauncher { + + private static readonly WAIT_MESSAGE = nls.localize('press.any.key', "Press any key to continue..."); + + public runInTerminal0(title: string, dir: string, args: string[], envVars: env.IProcessEnvironment, configuration: ITerminalSettings): TPromise { + + const terminalConfig = configuration.external; + const execPromise = terminalConfig.linuxExec ? TPromise.as(terminalConfig.linuxExec) : getDefaultTerminalLinuxReady(); + + return new TPromise((c, e) => { + + let termArgs: string[] = []; + //termArgs.push('--title'); + //termArgs.push(`"${TERMINAL_TITLE}"`); + execPromise.then(exec => { + if (exec.indexOf('gnome-terminal') >= 0) { + termArgs.push('-x'); + } else { + termArgs.push('-e'); + } + termArgs.push('bash'); + termArgs.push('-c'); + + const bashCommand = `${quote(args)}; echo; read -p "${LinuxTerminalService.WAIT_MESSAGE}" -n1;`; + termArgs.push(`''${bashCommand}''`); // wrapping argument in two sets of ' because node is so "friendly" that it removes one set... + + // merge environment variables into a copy of the process.env + const env = assign({}, process.env, envVars); + + // delete environment variables that have a null value + Object.keys(env).filter(v => env[v] === null).forEach(key => delete env[key]); + + const options: any = { + cwd: dir, + env: env + }; + + let stderr = ''; + const cmd = cp.spawn(exec, termArgs, options); + cmd.on('error', e); + cmd.stderr.on('data', (data) => { + stderr += data.toString(); + }); + cmd.on('exit', (code: number) => { + if (code === 0) { // OK + c(null); + } else { + if (stderr) { + const lines = stderr.split('\n', 1); + e(new Error(lines[0])); + } else { + e(new Error(nls.localize('linux.term.failed', "'{0}' failed with exit code {1}", exec, code))); + } + } + }); + }); + }); + } +} + +/** + * Quote args if necessary and combine into a space separated string. + */ +function quote(args: string[]): string { + let r = ''; + for (let a of args) { + if (a.indexOf(' ') >= 0) { + r += '"' + a + '"'; + } else { + r += a; + } + r += ' '; + } + return r; +} diff --git a/src/vs/workbench/parts/debug/node/v8Protocol.ts b/src/vs/workbench/parts/debug/node/v8Protocol.ts deleted file mode 100644 index cd9c151d7cb..00000000000 --- a/src/vs/workbench/parts/debug/node/v8Protocol.ts +++ /dev/null @@ -1,155 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as stream from 'stream'; -import { TPromise } from 'vs/base/common/winjs.base'; -import { canceled } from 'vs/base/common/errors'; - -export abstract class V8Protocol { - - private static readonly TWO_CRLF = '\r\n\r\n'; - - private outputStream: stream.Writable; - private sequence: number; - private pendingRequests: Map void>; - private rawData: Buffer; - private contentLength: number; - - constructor(private id: string) { - this.sequence = 1; - this.contentLength = -1; - this.pendingRequests = new Map void>(); - this.rawData = Buffer.allocUnsafe(0); - } - - public getId(): string { - return this.id; - } - - protected abstract onServerError(err: Error): void; - protected abstract onEvent(event: DebugProtocol.Event): void; - protected abstract dispatchRequest(request: DebugProtocol.Request, response: DebugProtocol.Response): void; - - protected connect(readable: stream.Readable, writable: stream.Writable): void { - - this.outputStream = writable; - - readable.on('data', (data: Buffer) => { - this.rawData = Buffer.concat([this.rawData, data]); - this.handleData(); - }); - } - - protected send(command: string, args: any): TPromise { - let errorCallback: (error: Error) => void; - return new TPromise((completeDispatch, errorDispatch) => { - errorCallback = errorDispatch; - this.doSend(command, args, (result: R) => { - if (result.success) { - completeDispatch(result); - } else { - errorDispatch(result); - } - }); - }, () => errorCallback(canceled())); - } - - public sendResponse(response: DebugProtocol.Response): void { - if (response.seq > 0) { - console.error(`attempt to send more than one response for command ${response.command}`); - } else { - this.sendMessage('response', response); - } - } - - private doSend(command: string, args: any, clb: (result: DebugProtocol.Response) => void): void { - - const request: any = { - command: command - }; - if (args && Object.keys(args).length > 0) { - request.arguments = args; - } - - this.sendMessage('request', request); - - if (clb) { - // store callback for this request - this.pendingRequests.set(request.seq, clb); - } - } - - private sendMessage(typ: 'request' | 'response' | 'event', message: DebugProtocol.ProtocolMessage): void { - - message.type = typ; - message.seq = this.sequence++; - - const json = JSON.stringify(message); - const length = Buffer.byteLength(json, 'utf8'); - - this.outputStream.write('Content-Length: ' + length.toString() + V8Protocol.TWO_CRLF, 'utf8'); - this.outputStream.write(json, 'utf8'); - } - - private handleData(): void { - while (true) { - if (this.contentLength >= 0) { - if (this.rawData.length >= this.contentLength) { - const message = this.rawData.toString('utf8', 0, this.contentLength); - this.rawData = this.rawData.slice(this.contentLength); - this.contentLength = -1; - if (message.length > 0) { - this.dispatch(message); - } - continue; // there may be more complete messages to process - } - } else { - const s = this.rawData.toString('utf8', 0, this.rawData.length); - const idx = s.indexOf(V8Protocol.TWO_CRLF); - if (idx !== -1) { - const match = /Content-Length: (\d+)/.exec(s); - if (match && match[1]) { - this.contentLength = Number(match[1]); - this.rawData = this.rawData.slice(idx + V8Protocol.TWO_CRLF.length); - continue; // try to handle a complete message - } - } - } - break; - } - } - - private dispatch(body: string): void { - try { - const rawData = JSON.parse(body); - switch (rawData.type) { - case 'event': - this.onEvent(rawData); - break; - case 'response': - const response = rawData; - const clb = this.pendingRequests.get(response.request_seq); - if (clb) { - this.pendingRequests.delete(response.request_seq); - clb(response); - } - break; - case 'request': - const request = rawData; - const resp: DebugProtocol.Response = { - type: 'response', - seq: 0, - command: request.command, - request_seq: request.seq, - success: true - }; - this.dispatchRequest(request, resp); - break; - } - } catch (e) { - this.onServerError(new Error(e.message || e)); - } - } -} diff --git a/src/vs/workbench/parts/debug/test/common/mockDebug.ts b/src/vs/workbench/parts/debug/test/common/mockDebug.ts index e626309a642..6db8bef34f6 100644 --- a/src/vs/workbench/parts/debug/test/common/mockDebug.ts +++ b/src/vs/workbench/parts/debug/test/common/mockDebug.ts @@ -111,6 +111,7 @@ export class MockDebugService implements IDebugService { } export class MockSession implements ISession { + public readyForBreakpoints = true; public emittedStopped = true; @@ -217,6 +218,10 @@ export class MockSession implements ISession { return TPromise.as(null); } + public terminateThreads(args: DebugProtocol.TerminateThreadsArguments): TPromise { + return TPromise.as(null); + } + public setVariable(args: DebugProtocol.SetVariableArguments): TPromise { return TPromise.as(null); } diff --git a/src/vs/workbench/parts/debug/test/node/debugAdapter.test.ts b/src/vs/workbench/parts/debug/test/node/debugAdapter.test.ts deleted file mode 100644 index fadc7c6c5fd..00000000000 --- a/src/vs/workbench/parts/debug/test/node/debugAdapter.test.ts +++ /dev/null @@ -1,141 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as assert from 'assert'; -import * as paths from 'vs/base/common/paths'; -import * as platform from 'vs/base/common/platform'; -import { IRawAdapter, IAdapterExecutable, IConfigurationManager } from 'vs/workbench/parts/debug/common/debug'; -import { Adapter } from 'vs/workbench/parts/debug/node/debugAdapter'; -import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; -import uri from 'vs/base/common/uri'; -import { TPromise } from 'vs/base/common/winjs.base'; - - -suite('Debug - Adapter', () => { - let adapter: Adapter; - const extensionFolderPath = 'a/b/c/'; - const rawAdapter = { - type: 'mock', - label: 'Mock Debug', - enableBreakpointsFor: { 'languageIds': ['markdown'] }, - program: './out/mock/mockDebug.js', - args: ['arg1', 'arg2'], - configurationAttributes: { - launch: { - required: ['program'], - properties: { - program: { - 'type': 'string', - 'description': 'Workspace relative path to a text file.', - 'default': 'readme.md' - } - } - } - }, - variables: null, - initialConfigurations: [ - { - name: 'Mock-Debug', - type: 'mock', - request: 'launch', - program: 'readme.md' - } - ] - }; - const configurationManager = { - debugAdapterExecutable(folderUri: uri | undefined, type: string): TPromise { - return TPromise.as(undefined); - } - }; - - setup(() => { - adapter = new Adapter(configurationManager, rawAdapter, { extensionFolderPath, id: 'adapter', name: 'myAdapter', version: '1.0.0', publisher: 'vscode', isBuiltin: false, engines: null }, - new TestConfigurationService(), null); - }); - - teardown(() => { - adapter = null; - }); - - test('attributes', () => { - assert.equal(adapter.type, rawAdapter.type); - assert.equal(adapter.label, rawAdapter.label); - - return adapter.getAdapterExecutable(undefined, false).then(details => { - assert.equal(details.command, paths.join(extensionFolderPath, rawAdapter.program)); - assert.deepEqual(details.args, rawAdapter.args); - }); - }); - - test('schema attributes', () => { - const schemaAttribute = adapter.getSchemaAttributes()[0]; - assert.notDeepEqual(schemaAttribute, rawAdapter.configurationAttributes); - Object.keys(rawAdapter.configurationAttributes.launch).forEach(key => { - assert.deepEqual(schemaAttribute[key], rawAdapter.configurationAttributes.launch[key]); - }); - - assert.equal(schemaAttribute['additionalProperties'], false); - assert.equal(!!schemaAttribute['properties']['request'], true); - assert.equal(!!schemaAttribute['properties']['name'], true); - assert.equal(!!schemaAttribute['properties']['type'], true); - assert.equal(!!schemaAttribute['properties']['preLaunchTask'], true); - }); - - test('merge', () => { - - const da: IRawAdapter = { - type: 'mock', - win: { - runtime: 'winRuntime' - }, - linux: { - runtime: 'linuxRuntime' - }, - osx: { - runtime: 'osxRuntime' - }, - runtimeArgs: ['first arg'], - program: 'mockprogram', - args: ['arg'] - }; - - adapter.merge(da, { - name: 'my name', - id: 'my_id', - version: '1.0', - publisher: 'mockPublisher', - isBuiltin: true, - extensionFolderPath: 'a/b/c/d', - engines: null - }); - - return adapter.getAdapterExecutable(undefined, false).then(details => { - assert.equal(details.command, platform.isLinux ? da.linux.runtime : platform.isMacintosh ? da.osx.runtime : da.win.runtime); - assert.deepEqual(details.args, da.runtimeArgs.concat(['a/b/c/d/mockprogram'].concat(da.args))); - }); - }); - - test('initial config file content', () => { - - const expected = ['{', - ' // Use IntelliSense to learn about possible attributes.', - ' // Hover to view descriptions of existing attributes.', - ' // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387', - ' "version": "0.2.0",', - ' "configurations": [', - ' {', - ' "name": "Mock-Debug",', - ' "type": "mock",', - ' "request": "launch",', - ' "program": "readme.md"', - ' }', - ' ]', - '}'].join('\n'); - - return adapter.getInitialConfigurationContent().then(content => { - assert.equal(content, expected); - }, err => assert.fail()); - }); -}); \ No newline at end of file diff --git a/src/vs/workbench/parts/debug/test/node/debugger.test.ts b/src/vs/workbench/parts/debug/test/node/debugger.test.ts new file mode 100644 index 00000000000..0e7dadf2ea2 --- /dev/null +++ b/src/vs/workbench/parts/debug/test/node/debugger.test.ts @@ -0,0 +1,182 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import * as paths from 'vs/base/common/paths'; +import * as platform from 'vs/base/common/platform'; +import { IAdapterExecutable, IConfigurationManager } from 'vs/workbench/parts/debug/common/debug'; +import { Debugger } from 'vs/workbench/parts/debug/node/debugger'; +import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; +import uri from 'vs/base/common/uri'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { DebugAdapter } from 'vs/workbench/parts/debug/node/debugAdapter'; + + +suite('Debug - Debugger', () => { + let _debugger: Debugger; + + const extensionFolderPath = 'a/b/c/'; + const debuggerContribution = { + type: 'mock', + label: 'Mock Debug', + enableBreakpointsFor: { 'languageIds': ['markdown'] }, + program: './out/mock/mockDebug.js', + args: ['arg1', 'arg2'], + configurationAttributes: { + launch: { + required: ['program'], + properties: { + program: { + 'type': 'string', + 'description': 'Workspace relative path to a text file.', + 'default': 'readme.md' + } + } + } + }, + variables: null, + initialConfigurations: [ + { + name: 'Mock-Debug', + type: 'mock', + request: 'launch', + program: 'readme.md' + } + ] + }; + + const extensionDescriptor0 = { + id: 'adapter', + name: 'myAdapter', + version: '1.0.0', + publisher: 'vscode', + extensionFolderPath: extensionFolderPath, + isBuiltin: false, + engines: null, + contributes: { + 'debuggers': [ + debuggerContribution + ] + } + }; + + const extensionDescriptor1 = { + id: 'extension1', + name: 'extension1', + version: '1.0.0', + publisher: 'vscode', + extensionFolderPath: '/e1/b/c/', + isBuiltin: false, + engines: null, + contributes: { + 'debuggers': [ + { + type: 'mock', + runtime: 'runtime', + runtimeArgs: ['rarg'], + program: 'mockprogram', + args: ['parg'] + } + ] + } + }; + + const extensionDescriptor2 = { + id: 'extension2', + name: 'extension2', + version: '1.0.0', + publisher: 'vscode', + extensionFolderPath: '/e2/b/c/', + isBuiltin: false, + engines: null, + contributes: { + 'debuggers': [ + { + type: 'mock', + win: { + runtime: 'winRuntime', + program: 'winProgram' + }, + linux: { + runtime: 'linuxRuntime', + program: 'linuxProgram' + }, + osx: { + runtime: 'osxRuntime', + program: 'osxProgram' + } + } + ] + } + }; + + + const configurationManager = { + debugAdapterExecutable(folderUri: uri | undefined, type: string): TPromise { + return TPromise.as(undefined); + } + }; + + setup(() => { + _debugger = new Debugger(configurationManager, debuggerContribution, extensionDescriptor0, new TestConfigurationService(), null, null); + }); + + teardown(() => { + _debugger = null; + }); + + test('attributes', () => { + assert.equal(_debugger.type, debuggerContribution.type); + assert.equal(_debugger.label, debuggerContribution.label); + + const ae = DebugAdapter.platformAdapterExecutable([extensionDescriptor0], 'mock'); + + assert.equal(ae.command, paths.join(extensionFolderPath, debuggerContribution.program)); + assert.deepEqual(ae.args, debuggerContribution.args); + }); + + test('schema attributes', () => { + const schemaAttribute = _debugger.getSchemaAttributes()[0]; + assert.notDeepEqual(schemaAttribute, debuggerContribution.configurationAttributes); + Object.keys(debuggerContribution.configurationAttributes.launch).forEach(key => { + assert.deepEqual(schemaAttribute[key], debuggerContribution.configurationAttributes.launch[key]); + }); + + assert.equal(schemaAttribute['additionalProperties'], false); + assert.equal(!!schemaAttribute['properties']['request'], true); + assert.equal(!!schemaAttribute['properties']['name'], true); + assert.equal(!!schemaAttribute['properties']['type'], true); + assert.equal(!!schemaAttribute['properties']['preLaunchTask'], true); + }); + + test('merge platform specific attributes', () => { + const ae = DebugAdapter.platformAdapterExecutable([extensionDescriptor1, extensionDescriptor2], 'mock'); + assert.equal(ae.command, platform.isLinux ? 'linuxRuntime' : (platform.isMacintosh ? 'osxRuntime' : 'winRuntime')); + const xprogram = platform.isLinux ? 'linuxProgram' : (platform.isMacintosh ? 'osxProgram' : 'winProgram'); + assert.deepEqual(ae.args, ['rarg', '/e2/b/c/' + xprogram, 'parg']); + }); + + test('initial config file content', () => { + + const expected = ['{', + ' // Use IntelliSense to learn about possible attributes.', + ' // Hover to view descriptions of existing attributes.', + ' // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387', + ' "version": "0.2.0",', + ' "configurations": [', + ' {', + ' "name": "Mock-Debug",', + ' "type": "mock",', + ' "request": "launch",', + ' "program": "readme.md"', + ' }', + ' ]', + '}'].join('\n'); + + return _debugger.getInitialConfigurationContent().then(content => { + assert.equal(content, expected); + }, err => assert.fail(err)); + }); +}); \ No newline at end of file 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 003b60d75b2..21804198294 100644 --- a/src/vs/workbench/parts/execution/electron-browser/execution.contribution.ts +++ b/src/vs/workbench/parts/execution/electron-browser/execution.contribution.ts @@ -15,7 +15,7 @@ import { ITerminalService } from 'vs/workbench/parts/execution/common/execution' import { MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; -import { Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; +import { Extensions, IConfigurationRegistry, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; import { ITerminalService as IIntegratedTerminalService, KEYBINDING_CONTEXT_TERMINAL_NOT_FOCUSED } from 'vs/workbench/parts/terminal/common/terminal'; import { getDefaultTerminalWindows, getDefaultTerminalLinuxReady, DEFAULT_TERMINAL_OSX, ITerminalConfiguration } from 'vs/workbench/parts/execution/electron-browser/terminal'; import { WinTerminalService, MacTerminalService, LinuxTerminalService } from 'vs/workbench/parts/execution/electron-browser/terminalService'; @@ -52,26 +52,25 @@ getDefaultTerminalLinuxReady().then(defaultTerminalLinux => { 'external' ], 'description': nls.localize('explorer.openInTerminalKind', "Customizes what kind of terminal to launch."), - 'default': 'integrated', - 'isExecutable': false + 'default': 'integrated' }, 'terminal.external.windowsExec': { 'type': 'string', 'description': nls.localize('terminal.external.windowsExec', "Customizes which terminal to run on Windows."), 'default': getDefaultTerminalWindows(), - 'isExecutable': true + 'scope': ConfigurationScope.APPLICATION }, '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 + 'scope': ConfigurationScope.APPLICATION }, 'terminal.external.linuxExec': { 'type': 'string', 'description': nls.localize('terminal.external.linuxExec', "Customizes which terminal to run on Linux."), 'default': defaultTerminalLinux, - 'isExecutable': true + 'scope': ConfigurationScope.APPLICATION } } }); @@ -92,7 +91,7 @@ CommandsRegistry.registerCommand({ const directoriesToOpen = distinct(stats.map(({ stat }) => stat.isDirectory ? stat.resource.fsPath : paths.dirname(stat.resource.fsPath))); return directoriesToOpen.map(dir => { if (configurationService.getValue().terminal.explorerKind === 'integrated') { - const instance = integratedTerminalService.createInstance({ cwd: dir }, true); + const instance = integratedTerminalService.createTerminal({ cwd: dir }, true); if (instance && (resources.length === 1 || !resource || dir === resource.fsPath || dir === paths.dirname(resource.fsPath))) { integratedTerminalService.setActiveInstance(instance); integratedTerminalService.showPanel(true); @@ -117,6 +116,12 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ const root = historyService.getLastActiveWorkspaceRoot(Schemas.file); if (root) { terminalService.openTerminal(root.fsPath); + } else { + // Opens current file's folder, if no folder is open in editor + const activeFile = historyService.getLastActiveFile(); + if (activeFile) { + terminalService.openTerminal(paths.dirname(activeFile.fsPath)); + } } } }); diff --git a/src/vs/workbench/parts/execution/electron-browser/terminal.ts b/src/vs/workbench/parts/execution/electron-browser/terminal.ts index c4230a35fdf..c6dab0141ca 100644 --- a/src/vs/workbench/parts/execution/electron-browser/terminal.ts +++ b/src/vs/workbench/parts/execution/electron-browser/terminal.ts @@ -43,7 +43,7 @@ let _DEFAULT_TERMINAL_WINDOWS: string = null; export function getDefaultTerminalWindows(): string { if (!_DEFAULT_TERMINAL_WINDOWS) { const isWoW64 = !!process.env.hasOwnProperty('PROCESSOR_ARCHITEW6432'); - _DEFAULT_TERMINAL_WINDOWS = `${process.env.windir ? process.env.windir : 'C:'}\\${isWoW64 ? 'Sysnative' : 'System32'}\\cmd.exe`; + _DEFAULT_TERMINAL_WINDOWS = `${process.env.windir ? process.env.windir : 'C:\\Windows'}\\${isWoW64 ? 'Sysnative' : 'System32'}\\cmd.exe`; } return _DEFAULT_TERMINAL_WINDOWS; } diff --git a/src/vs/workbench/parts/extensions/browser/extensionsActions.ts b/src/vs/workbench/parts/extensions/browser/extensionsActions.ts index 0d5e207078e..df3e59bcee6 100644 --- a/src/vs/workbench/parts/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/parts/extensions/browser/extensionsActions.ts @@ -10,7 +10,7 @@ import { IAction, Action } from 'vs/base/common/actions'; import { Throttler } from 'vs/base/common/async'; import * as DOM from 'vs/base/browser/dom'; import * as paths from 'vs/base/common/paths'; -import { Event, once } from 'vs/base/common/event'; +import { Event } from 'vs/base/common/event'; import * as json from 'vs/base/common/json'; import { ActionItem, IActionItem, Separator } from 'vs/base/browser/ui/actionbar/actionbar'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; @@ -48,31 +48,25 @@ import { mnemonicButtonLabel } from 'vs/base/common/labels'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IQuickOpenService, IPickOpenEntry } from 'vs/platform/quickOpen/common/quickOpen'; -class DownloadExtensionAction extends Action { - - constructor( - private extension: IExtension, - @IOpenerService private openerService: IOpenerService, - @INotificationService private notificationService: INotificationService, - @IInstantiationService private instantiationService: IInstantiationService - ) { - super('extensions.download', localize('download', "Download Manually"), '', true); - } - - run(): TPromise { - return this.openerService.open(URI.parse(this.extension.downloadUrl)).then(() => { - const action = this.instantiationService.createInstance(InstallVSIXAction, InstallVSIXAction.ID, InstallVSIXAction.LABEL); - const handle = this.notificationService.notify({ - severity: Severity.Info, - message: localize('install vsix', 'Once downloaded, please manually install the downloaded VSIX of \'{0}\'.', this.extension.id), - actions: { - primary: [action] - } - }); - once(handle.onDidDispose)(() => action.dispose()); - }); - } -} +const promptDownloadManually = (extension: IExtension, message: string, instantiationService: IInstantiationService, notificationService: INotificationService, openerService: IOpenerService) => { + notificationService.prompt(Severity.Error, message, [{ + label: localize('download', "Download Manually"), + run: () => openerService.open(URI.parse(extension.downloadUrl)).then(() => { + notificationService.prompt( + Severity.Info, + localize('install vsix', 'Once downloaded, please manually install the downloaded VSIX of \'{0}\'.', extension.id), + [{ + label: InstallVSIXAction.LABEL, + run: () => { + const action = instantiationService.createInstance(InstallVSIXAction, InstallVSIXAction.ID, InstallVSIXAction.LABEL); + action.run(); + action.dispose(); + } + }] + ); + }) + }]); +}; export class InstallAction extends Action { @@ -90,7 +84,8 @@ export class InstallAction extends Action { constructor( @IExtensionsWorkbenchService private extensionsWorkbenchService: IExtensionsWorkbenchService, @IInstantiationService private instantiationService: IInstantiationService, - @INotificationService private notificationService: INotificationService + @INotificationService private notificationService: INotificationService, + @IOpenerService private openerService: IOpenerService ) { super('extensions.install', InstallAction.InstallLabel, InstallAction.Class, false); @@ -133,15 +128,7 @@ export class InstallAction extends Action { console.error(err); - const action = this.instantiationService.createInstance(DownloadExtensionAction, extension); - const handle = this.notificationService.notify({ - severity: Severity.Error, - message: localize('failedToInstall', "Failed to install \'{0}\'.", extension.id), - actions: { - primary: [action] - } - }); - once(handle.onDidDispose)(() => action.dispose()); + promptDownloadManually(extension, localize('failedToInstall', "Failed to install \'{0}\'.", extension.id), this.instantiationService, this.notificationService, this.openerService); }); } @@ -306,7 +293,8 @@ export class UpdateAction extends Action { constructor( @IExtensionsWorkbenchService private extensionsWorkbenchService: IExtensionsWorkbenchService, @IInstantiationService private instantiationService: IInstantiationService, - @INotificationService private notificationService: INotificationService + @INotificationService private notificationService: INotificationService, + @IOpenerService private openerService: IOpenerService ) { super('extensions.update', UpdateAction.Label, UpdateAction.DisabledClass, false); @@ -348,15 +336,8 @@ export class UpdateAction extends Action { } console.error(err); - const action = this.instantiationService.createInstance(DownloadExtensionAction, extension); - const handle = this.notificationService.notify({ - severity: Severity.Error, - message: localize('failedToUpdate', "Failed to update \'{0}\'.", extension.id), - actions: { - primary: [action] - } - }); - once(handle.onDidDispose)(() => action.dispose()); + + promptDownloadManually(extension, localize('failedToUpdate', "Failed to update \'{0}\'.", extension.id), this.instantiationService, this.notificationService, this.openerService); }); } @@ -833,7 +814,8 @@ export class UpdateAllAction extends Action { label = UpdateAllAction.LABEL, @IExtensionsWorkbenchService private extensionsWorkbenchService: IExtensionsWorkbenchService, @INotificationService private notificationService: INotificationService, - @IInstantiationService private instantiationService: IInstantiationService + @IInstantiationService private instantiationService: IInstantiationService, + @IOpenerService private openerService: IOpenerService ) { super(id, label, '', false); @@ -860,15 +842,8 @@ export class UpdateAllAction extends Action { } console.error(err); - const action = this.instantiationService.createInstance(DownloadExtensionAction, extension); - const handle = this.notificationService.notify({ - severity: Severity.Error, - message: localize('failedToUpdate', "Failed to update \'{0}\'.", extension.id), - actions: { - primary: [action] - } - }); - once(handle.onDidDispose)(() => action.dispose()); + + promptDownloadManually(extension, localize('failedToUpdate', "Failed to update \'{0}\'.", extension.id), this.instantiationService, this.notificationService, this.openerService); }); } @@ -1205,7 +1180,8 @@ export class InstallWorkspaceRecommendedExtensionsAction extends Action { @IExtensionsWorkbenchService private extensionsWorkbenchService: IExtensionsWorkbenchService, @IExtensionTipsService private extensionTipsService: IExtensionTipsService, @INotificationService private notificationService: INotificationService, - @IInstantiationService private instantiationService: IInstantiationService + @IInstantiationService private instantiationService: IInstantiationService, + @IOpenerService private openerService: IOpenerService ) { super(id, label, 'extension-action'); this.extensionsWorkbenchService.onChange(() => this.update(), this, this.disposables); @@ -1269,15 +1245,8 @@ export class InstallWorkspaceRecommendedExtensionsAction extends Action { } console.error(err); - const action = this.instantiationService.createInstance(DownloadExtensionAction, extension); - const handle = this.notificationService.notify({ - severity: Severity.Error, - message: localize('failedToInstall', "Failed to install \'{0}\'.", extension.id), - actions: { - primary: [action] - } - }); - once(handle.onDidDispose)(() => action.dispose()); + + promptDownloadManually(extension, localize('failedToInstall', "Failed to install \'{0}\'.", extension.id), this.instantiationService, this.notificationService, this.openerService); }); } @@ -1300,7 +1269,8 @@ export class InstallRecommendedExtensionAction extends Action { @IViewletService private viewletService: IViewletService, @IExtensionsWorkbenchService private extensionsWorkbenchService: IExtensionsWorkbenchService, @INotificationService private notificationService: INotificationService, - @IInstantiationService private instantiationService: IInstantiationService + @IInstantiationService private instantiationService: IInstantiationService, + @IOpenerService private openerService: IOpenerService ) { super(InstallRecommendedExtensionAction.ID, InstallRecommendedExtensionAction.LABEL, null); this.extensionId = extensionId; @@ -1338,15 +1308,8 @@ export class InstallRecommendedExtensionAction extends Action { } console.error(err); - const action = this.instantiationService.createInstance(DownloadExtensionAction, extension); - const handle = this.notificationService.notify({ - severity: Severity.Error, - message: localize('failedToInstall', "Failed to install \'{0}\'.", extension.id), - actions: { - primary: [action] - } - }); - once(handle.onDidDispose)(() => action.dispose()); + + promptDownloadManually(extension, localize('failedToInstall', "Failed to install \'{0}\'.", extension.id), this.instantiationService, this.notificationService, this.openerService); }); } @@ -1921,13 +1884,13 @@ export class InstallVSIXAction extends Action { label = InstallVSIXAction.LABEL, @IExtensionsWorkbenchService private extensionsWorkbenchService: IExtensionsWorkbenchService, @INotificationService private notificationService: INotificationService, - @IWindowService private windowsService: IWindowService + @IWindowService private windowService: IWindowService ) { super(id, label, 'extension-action install-vsix', true); } run(): TPromise { - return this.windowsService.showOpenDialog({ + return this.windowService.showOpenDialog({ title: localize('installFromVSIX', "Install from VSIX"), filters: [{ name: 'VSIX Extensions', extensions: ['vsix'] }], properties: ['openFile'], @@ -1938,19 +1901,19 @@ export class InstallVSIXAction extends Action { } return TPromise.join(result.map(vsix => this.extensionsWorkbenchService.install(vsix))).then(() => { - return this.notificationService.prompt(Severity.Info, localize('InstallVSIXAction.success', "Successfully installed the extension. Reload to enable it."), [localize('InstallVSIXAction.reloadNow', "Reload Now")]).then(choice => { - if (choice === 0) { - return this.windowsService.reloadWindow(); - } - - return TPromise.as(undefined); - }); + this.notificationService.prompt( + Severity.Info, + localize('InstallVSIXAction.success', "Successfully installed the extension. Reload to enable it."), + [{ + label: localize('InstallVSIXAction.reloadNow', "Reload Now"), + run: () => this.windowService.reloadWindow() + }] + ); }); }); } } - export class ReinstallAction extends Action { static readonly ID = 'workbench.extensions.action.reinstall'; @@ -1994,19 +1957,14 @@ export class ReinstallAction extends Action { private reinstallExtension(extension: IExtension): TPromise { return this.extensionsWorkbenchService.reinstall(extension) .then(() => { - this.notificationService.notify({ - message: localize('ReinstallAction.success', "Successfully reinstalled the extension."), - severity: Severity.Info, - actions: { - primary: [{ - id: 'reload', - label: localize('ReinstallAction.reloadNow', "Reload Now"), - enabled: true, - run: () => this.windowService.reloadWindow(), - dispose: () => null - }] - } - }); + this.notificationService.prompt( + Severity.Info, + localize('ReinstallAction.success', "Successfully reinstalled the extension."), + [{ + label: localize('ReinstallAction.reloadNow', "Reload Now"), + run: () => this.windowService.reloadWindow() + }] + ); }, error => this.notificationService.error(error)); } } diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensionEditor.ts b/src/vs/workbench/parts/extensions/electron-browser/extensionEditor.ts index 08780638219..ea7188eab8e 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/extensionEditor.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/extensionEditor.ts @@ -17,7 +17,6 @@ import Cache from 'vs/base/common/cache'; import { Action } from 'vs/base/common/actions'; import { isPromiseCanceledError } from 'vs/base/common/errors'; import { IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; -import { Builder } from 'vs/base/browser/builder'; import { domEvent } from 'vs/base/browser/event'; import { append, $, addClass, removeClass, finalHandler, join, toggleClass } from 'vs/base/browser/dom'; import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; @@ -33,7 +32,7 @@ import { RatingsWidget, InstallCountWidget } from 'vs/workbench/parts/extensions import { EditorOptions } from 'vs/workbench/common/editor'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { CombinedInstallAction, UpdateAction, EnableAction, DisableAction, ReloadAction, MaliciousStatusLabelAction, DisabledStatusLabelAction } from 'vs/workbench/parts/extensions/browser/extensionsActions'; -import { Webview } from 'vs/workbench/parts/html/electron-browser/webview'; +import { WebviewElement } from 'vs/workbench/parts/webview/electron-browser/webviewElement'; import { KeybindingIO } from 'vs/workbench/services/keybinding/common/keybindingIO'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; @@ -178,7 +177,7 @@ export class ExtensionEditor extends BaseEditor { private contentDisposables: IDisposable[] = []; private transientDisposables: IDisposable[] = []; private disposables: IDisposable[]; - private activeWebview: Webview; + private activeWebview: WebviewElement; constructor( @ITelemetryService telemetryService: ITelemetryService, @@ -206,10 +205,8 @@ export class ExtensionEditor extends BaseEditor { this.findInputFocusContextKey = KEYBINDING_CONTEXT_EXTENSIONEDITOR_FIND_WIDGET_INPUT_FOCUSED.bindTo(this.contextKeyService); } - createEditor(parent: Builder): void { - const container = parent.getHTMLElement(); - - const root = append(container, $('.extension-editor')); + createEditor(parent: HTMLElement): void { + const root = append(parent, $('.extension-editor')); this.header = append(root, $('.header')); this.icon = append(this.header, $('img.icon', { draggable: false })); @@ -428,7 +425,7 @@ export class ExtensionEditor extends BaseEditor { .then(body => { const allowedBadgeProviders = this.extensionsWorkbenchService.allowedBadgeProviders; const webViewOptions = allowedBadgeProviders.length > 0 ? { allowScripts: false, allowSvgs: false, svgWhiteList: allowedBadgeProviders } : {}; - this.activeWebview = new Webview(this.partService.getContainer(Parts.EDITOR_PART), this.themeService, this.environmentService, this.contextViewService, this.contextKey, this.findInputFocusContextKey, webViewOptions); + this.activeWebview = new WebviewElement(this.partService.getContainer(Parts.EDITOR_PART), this.themeService, this.environmentService, this.contextViewService, this.contextKey, this.findInputFocusContextKey, webViewOptions); this.activeWebview.mountTo(this.content); const removeLayoutParticipant = arrays.insert(this.layoutParticipants, this.activeWebview); this.contentDisposables.push(toDisposable(removeLayoutParticipant)); @@ -642,7 +639,7 @@ export class ExtensionEditor extends BaseEditor { const details = $('details', { open: true, ontoggle: onDetailsToggle }, $('summary', null, localize('localizations', "Localizations ({0})", localizations.length)), $('table', null, - $('tr', null, $('th', null, localize('localizations language id', "Language Id")), $('th', null, localize('localizations language name', "Langauge Name")), $('th', null, localize('localizations localized language name', "Langauge Name (Localized)"))), + $('tr', null, $('th', null, localize('localizations language id', "Language Id")), $('th', null, localize('localizations language name', "Language Name")), $('th', null, localize('localizations localized language name', "Language Name (Localized)"))), ...localizations.map(localization => $('tr', null, $('td', null, localization.languageId), $('td', null, localization.languageName), $('td', null, localization.languageNameLocalized))) ) ); diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensionTipsService.ts b/src/vs/workbench/parts/extensions/electron-browser/extensionTipsService.ts index 0b7b540ca90..93215f58bfd 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/extensionTipsService.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/extensionTipsService.ts @@ -36,7 +36,7 @@ import { asJson } from 'vs/base/node/request'; import { isNumber } from 'vs/base/common/types'; import { language, LANGUAGE_DEFAULT } from 'vs/base/common/platform'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; -import { INotificationService, PromptOption } from 'vs/platform/notification/common/notification'; +import { INotificationService } from 'vs/platform/notification/common/notification'; interface IExtensionsContent { recommendations: string[]; @@ -160,15 +160,13 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe if (!pager || !pager.firstPage || !pager.firstPage.length) { return; } - const message = localize('showLanguagePackExtensions', "The Marketplace has extensions that can help localizing VS Code to '{0}' locale", language); - const options: PromptOption[] = [ - searchMarketplace, - { label: choiceNever } - ]; - this.notificationService.prompt(Severity.Info, message, options).done(choice => { - switch (choice) { - case 0 /* Search Marketplace */: + this.notificationService.prompt( + Severity.Info, + localize('showLanguagePackExtensions', "The Marketplace has extensions that can help localizing VS Code to '{0}' locale", language), + [{ + label: searchMarketplace, + run: () => { /* __GDPR__ "languagePackSuggestion:popup" : { "userReaction" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, @@ -182,8 +180,12 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe viewlet.search(`tag:lp-${language}`); viewlet.focus(); }); - break; - case 1 /* Never show again */: + } + }, + { + label: choiceNever, + isSecondary: true, + run: () => { languagePackSuggestionIgnoreList.push(language); this.storageService.store( 'extensionsAssistant/languagePackSuggestionIgnore', @@ -197,17 +199,18 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe } */ this.telemetryService.publicLog('languagePackSuggestion:popup', { userReaction: 'neverShowAgain', language }); - break; - } - }, () => { - /* __GDPR__ - "languagePackSuggestion:popup" : { - "userReaction" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "language": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" } } - */ - this.telemetryService.publicLog('languagePackSuggestion:popup', { userReaction: 'cancelled', language }); - }); + }], + () => { + /* __GDPR__ + "languagePackSuggestion:popup" : { + "userReaction" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "language": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" } + } + */ + this.telemetryService.publicLog('languagePackSuggestion:popup', { userReaction: 'cancelled', language }); + } + ); }); }); } @@ -487,26 +490,25 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe message = localize('reallyRecommendedExtensionPack', "The '{0}' extension pack is recommended for this file type.", name); } - const recommendationsAction = this.instantiationService.createInstance(ShowRecommendedExtensionsAction, ShowRecommendedExtensionsAction.ID, localize('showRecommendations', "Show Recommendations")); - const installAction = this.instantiationService.createInstance(InstallRecommendedExtensionAction, id); - const options: PromptOption[] = [ - localize('install', 'Install'), - recommendationsAction.label, - { label: choiceNever } - ]; - - this.notificationService.prompt(Severity.Info, message, options).done(choice => { - switch (choice) { - case 0 /* Install */: + this.notificationService.prompt(Severity.Info, message, + [{ + label: localize('install', 'Install'), + run: () => { /* __GDPR__ - "extensionRecommendations:popup" : { - "userReaction" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "extensionId": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" } - } + "extensionRecommendations:popup" : { + "userReaction" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "extensionId": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" } + } */ this.telemetryService.publicLog('extensionRecommendations:popup', { userReaction: 'install', extensionId: name }); - return installAction.run(); - case 1 /* Show Recommendations */: + + const installAction = this.instantiationService.createInstance(InstallRecommendedExtensionAction, id); + installAction.run(); + installAction.dispose(); + } + }, { + label: localize('showRecommendations', "Show Recommendations"), + run: () => { /* __GDPR__ "extensionRecommendations:popup" : { "userReaction" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, @@ -514,8 +516,15 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe } */ this.telemetryService.publicLog('extensionRecommendations:popup', { userReaction: 'show', extensionId: name }); - return recommendationsAction.run(); - case 2 /* Never show again */: + + const recommendationsAction = this.instantiationService.createInstance(ShowRecommendedExtensionsAction, ShowRecommendedExtensionsAction.ID, localize('showRecommendations', "Show Recommendations")); + recommendationsAction.run(); + recommendationsAction.dispose(); + } + }, { + label: choiceNever, + isSecondary: true, + run: () => { importantRecommendationsIgnoreList.push(id); this.storageService.store( 'extensionsAssistant/importantRecommendationsIgnore', @@ -529,17 +538,19 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe } */ this.telemetryService.publicLog('extensionRecommendations:popup', { userReaction: 'neverShowAgain', extensionId: name }); - return this.ignoreExtensionRecommendations(); - } - }, () => { - /* __GDPR__ - "extensionRecommendations:popup" : { - "userReaction" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "extensionId": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" } + this.ignoreExtensionRecommendations(); } - */ - this.telemetryService.publicLog('extensionRecommendations:popup', { userReaction: 'cancelled', extensionId: name }); - }); + }], + () => { + /* __GDPR__ + "extensionRecommendations:popup" : { + "userReaction" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "extensionId": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" } + } + */ + this.telemetryService.publicLog('extensionRecommendations:popup', { userReaction: 'cancelled', extensionId: name }); + } + ); }); const mimeTypesPromise = this.getMimeTypes(uri.fsPath); @@ -568,15 +579,12 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe return; } - const message = localize('showLanguageExtensions', "The Marketplace has extensions that can help with '.{0}' files", fileExtension); - const options: PromptOption[] = [ - searchMarketplace, - { label: choiceNever } - ]; - - this.notificationService.prompt(Severity.Info, message, options).done(choice => { - switch (choice) { - case 0 /* Search Marketplace */: + this.notificationService.prompt( + Severity.Info, + localize('showLanguageExtensions', "The Marketplace has extensions that can help with '.{0}' files", fileExtension), + [{ + label: searchMarketplace, + run: () => { /* __GDPR__ "fileExtensionSuggestion:popup" : { "userReaction" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, @@ -590,8 +598,11 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe viewlet.search(`ext:${fileExtension}`); viewlet.focus(); }); - break; - case 1 /* Never show again */: + } + }, { + label: choiceNever, + isSecondary: true, + run: () => { fileExtensionSuggestionIgnoreList.push(fileExtension); this.storageService.store( 'extensionsAssistant/fileExtensionsSuggestionIgnore', @@ -605,17 +616,18 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe } */ this.telemetryService.publicLog('fileExtensionSuggestion:popup', { userReaction: 'neverShowAgain', fileExtension: fileExtension }); - break; - } - }, () => { - /* __GDPR__ - "fileExtensionSuggestion:popup" : { - "userReaction" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "fileExtension": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" } } - */ - this.telemetryService.publicLog('fileExtensionSuggestion:popup', { userReaction: 'cancelled', fileExtension: fileExtension }); - }); + }], + () => { + /* __GDPR__ + "fileExtensionSuggestion:popup" : { + "userReaction" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "fileExtension": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" } + } + */ + this.telemetryService.publicLog('fileExtensionSuggestion:popup', { userReaction: 'cancelled', fileExtension: fileExtension }); + } + ); }); }); }); @@ -635,73 +647,88 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe .filter(id => local.every(local => `${local.manifest.publisher.toLowerCase()}.${local.manifest.name.toLowerCase()}` !== id)); if (!recommendations.length) { - return; + return TPromise.as(void 0); } - const message = localize('workspaceRecommended', "This workspace has extension recommendations."); - const showAction = this.instantiationService.createInstance(ShowRecommendedExtensionsAction, ShowRecommendedExtensionsAction.ID, localize('showRecommendations', "Show Recommendations")); - const installAllAction = this.instantiationService.createInstance(InstallWorkspaceRecommendedExtensionsAction, InstallWorkspaceRecommendedExtensionsAction.ID, localize('installAll', "Install All")); + return new TPromise(c => { + this.notificationService.prompt( + Severity.Info, + localize('workspaceRecommended', "This workspace has extension recommendations."), + [{ + label: localize('installAll', "Install All"), + run: () => { + /* __GDPR__ + "extensionRecommendations:popup" : { + "userReaction" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } + */ + this.telemetryService.publicLog('extensionWorkspaceRecommendations:popup', { userReaction: 'install' }); - const options: PromptOption[] = [ - installAllAction.label, - showAction.label, - { label: choiceNever } - ]; + const installAllAction = this.instantiationService.createInstance(InstallWorkspaceRecommendedExtensionsAction, InstallWorkspaceRecommendedExtensionsAction.ID, localize('installAll', "Install All")); + installAllAction.run(); + installAllAction.dispose(); - return this.notificationService.prompt(Severity.Info, message, options).done(choice => { - switch (choice) { - case 0 /* Install */: + c(void 0); + } + }, { + label: localize('showRecommendations', "Show Recommendations"), + run: () => { + /* __GDPR__ + "extensionRecommendations:popup" : { + "userReaction" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } + */ + this.telemetryService.publicLog('extensionWorkspaceRecommendations:popup', { userReaction: 'show' }); + + const showAction = this.instantiationService.createInstance(ShowRecommendedExtensionsAction, ShowRecommendedExtensionsAction.ID, localize('showRecommendations', "Show Recommendations")); + showAction.run(); + showAction.dispose(); + + c(void 0); + } + }, { + label: choiceNever, + isSecondary: true, + run: () => { + /* __GDPR__ + "extensionRecommendations:popup" : { + "userReaction" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } + */ + this.telemetryService.publicLog('extensionWorkspaceRecommendations:popup', { userReaction: 'neverShowAgain' }); + this.storageService.store(storageKey, true, StorageScope.WORKSPACE); + + c(void 0); + } + }], + () => { /* __GDPR__ "extensionRecommendations:popup" : { "userReaction" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } } */ - this.telemetryService.publicLog('extensionWorkspaceRecommendations:popup', { userReaction: 'install' }); - return installAllAction.run(); - case 1 /* Show Recommendations */: - /* __GDPR__ - "extensionRecommendations:popup" : { - "userReaction" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - } - */ - this.telemetryService.publicLog('extensionWorkspaceRecommendations:popup', { userReaction: 'show' }); - return showAction.run(); - case 2 /* Never show again */: - /* __GDPR__ - "extensionRecommendations:popup" : { - "userReaction" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - } - */ - this.telemetryService.publicLog('extensionWorkspaceRecommendations:popup', { userReaction: 'neverShowAgain' }); - return this.storageService.store(storageKey, true, StorageScope.WORKSPACE); - } - }, () => { - /* __GDPR__ - "extensionRecommendations:popup" : { - "userReaction" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + this.telemetryService.publicLog('extensionWorkspaceRecommendations:popup', { userReaction: 'cancelled' }); + + c(void 0); } - */ - this.telemetryService.publicLog('extensionWorkspaceRecommendations:popup', { userReaction: 'cancelled' }); + ); }); }); }); } private ignoreExtensionRecommendations() { - const message = localize('ignoreExtensionRecommendations', "Do you want to ignore all extension recommendations?"); - const options = [ - localize('ignoreAll', "Yes, Ignore All"), - localize('no', "No") - ]; - - this.notificationService.prompt(Severity.Info, message, options).done(choice => { - switch (choice) { - case 0: // If the user ignores the current message and selects different file type - return this.setIgnoreRecommendationsConfig(true); - case 1: - return this.setIgnoreRecommendationsConfig(false); - } - }); + this.notificationService.prompt( + Severity.Info, + localize('ignoreExtensionRecommendations', "Do you want to ignore all extension recommendations?"), + [{ + label: localize('ignoreAll', "Yes, Ignore All"), + run: () => this.setIgnoreRecommendationsConfig(true) + }, { + label: localize('no', "No"), + run: () => this.setIgnoreRecommendationsConfig(false) + }] + ); } private _suggestBasedOnExecutables(): TPromise { diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensions.contribution.ts b/src/vs/workbench/parts/extensions/electron-browser/extensions.contribution.ts index 6cdd37b47c3..20d875bae68 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/extensions.contribution.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/extensions.contribution.ts @@ -27,14 +27,14 @@ import { import { ExtensionsInput } from 'vs/workbench/parts/extensions/common/extensionsInput'; import { ViewletRegistry, Extensions as ViewletExtensions, ViewletDescriptor } from 'vs/workbench/browser/viewlet'; import { ExtensionEditor } from 'vs/workbench/parts/extensions/electron-browser/extensionEditor'; -import { StatusUpdater, ExtensionsViewlet, MaliciousExtensionChecker } from 'vs/workbench/parts/extensions/electron-browser/extensionsViewlet'; +import { StatusUpdater, ExtensionsViewlet, MaliciousExtensionChecker, ExtensionsViewletViewsContribution } from 'vs/workbench/parts/extensions/electron-browser/extensionsViewlet'; import { IQuickOpenRegistry, Extensions, QuickOpenHandlerDescriptor } from 'vs/workbench/browser/quickopen'; -import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry'; +import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; import * as jsonContributionRegistry from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; import { ExtensionsConfigurationSchema, ExtensionsConfigurationSchemaId } from 'vs/workbench/parts/extensions/common/extensionsFileTemplate'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { KeymapExtensions, BetterMergeDisabled } from 'vs/workbench/parts/extensions/electron-browser/extensionsUtils'; +import { KeymapExtensions } from 'vs/workbench/parts/extensions/electron-browser/extensionsUtils'; import { adoptToGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { GalleryExtensionsHandler, ExtensionsHandler } from 'vs/workbench/parts/extensions/browser/extensionsQuickOpen'; import { EditorDescriptor, IEditorRegistry, Extensions as EditorExtensions } from 'vs/workbench/browser/editor'; @@ -54,7 +54,7 @@ workbenchRegistry.registerWorkbenchContribution(StatusUpdater, LifecyclePhase.Ru workbenchRegistry.registerWorkbenchContribution(MaliciousExtensionChecker, LifecyclePhase.Eventually); workbenchRegistry.registerWorkbenchContribution(ConfigureRecommendedExtensionsCommandsContributor, LifecyclePhase.Eventually); workbenchRegistry.registerWorkbenchContribution(KeymapExtensions, LifecyclePhase.Running); -workbenchRegistry.registerWorkbenchContribution(BetterMergeDisabled, LifecyclePhase.Running); +workbenchRegistry.registerWorkbenchContribution(ExtensionsViewletViewsContribution, LifecyclePhase.Starting); Registry.as(OutputExtensions.OutputChannels) .registerChannel(ExtensionsChannelId, ExtensionsLabel); @@ -121,7 +121,7 @@ const viewletDescriptor = new ViewletDescriptor( VIEWLET_ID, localize('extensions', "Extensions"), 'extensions', - 100 + 4 ); Registry.as(ViewletExtensions.Viewlets) @@ -205,7 +205,8 @@ Registry.as(ConfigurationExtensions.Configuration) 'extensions.autoUpdate': { type: 'boolean', description: localize('extensionsAutoUpdate', "Automatically update extensions"), - default: true + default: true, + scope: ConfigurationScope.APPLICATION }, 'extensions.ignoreRecommendations': { type: 'boolean', diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensionsActions.ts b/src/vs/workbench/parts/extensions/electron-browser/extensionsActions.ts index aa3773bbe87..06d3286942b 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/extensionsActions.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/extensionsActions.ts @@ -5,7 +5,7 @@ import { localize } from 'vs/nls'; import { TPromise } from 'vs/base/common/winjs.base'; -import { Action, IAction } from 'vs/base/common/actions'; +import { Action } from 'vs/base/common/actions'; import * as paths from 'vs/base/common/paths'; import { IExtensionsWorkbenchService, IExtension } from 'vs/workbench/parts/extensions/common/extensions'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; @@ -59,13 +59,13 @@ export class InstallVSIXAction extends Action { label = InstallVSIXAction.LABEL, @IExtensionsWorkbenchService private extensionsWorkbenchService: IExtensionsWorkbenchService, @INotificationService private notificationService: INotificationService, - @IWindowService private windowsService: IWindowService + @IWindowService private windowService: IWindowService ) { super(id, label, 'extension-action install-vsix', true); } run(): TPromise { - return this.windowsService.showOpenDialog({ + return this.windowService.showOpenDialog({ title: localize('installFromVSIX', "Install from VSIX"), filters: [{ name: 'VSIX Extensions', extensions: ['vsix'] }], properties: ['openFile'], @@ -76,13 +76,14 @@ export class InstallVSIXAction extends Action { } return TPromise.join(result.map(vsix => this.extensionsWorkbenchService.install(vsix))).then(() => { - return this.notificationService.prompt(Severity.Info, localize('InstallVSIXAction.success', "Successfully installed the extension. Reload to enable it."), [localize('InstallVSIXAction.reloadNow', "Reload Now")]).then(choice => { - if (choice === 0) { - return this.windowsService.reloadWindow(); - } - - return TPromise.as(undefined); - }); + this.notificationService.prompt( + Severity.Info, + localize('InstallVSIXAction.success', "Successfully installed the extension. Reload to enable it."), + [{ + label: localize('InstallVSIXAction.reloadNow', "Reload Now"), + run: () => this.windowService.reloadWindow() + }] + ); }); }); } @@ -131,19 +132,14 @@ export class ReinstallAction extends Action { private reinstallExtension(extension: IExtension): TPromise { return this.extensionsWorkbenchService.reinstall(extension) .then(() => { - this.notificationService.notify({ - message: localize('ReinstallAction.success', "Successfully reinstalled the extension."), - severity: Severity.Info, - actions: { - primary: [{ - id: 'reload', - label: localize('ReinstallAction.reloadNow', "Reload Now"), - enabled: true, - run: () => this.windowService.reloadWindow(), - dispose: () => null - }] - } - }); + this.notificationService.prompt( + Severity.Info, + localize('ReinstallAction.success', "Successfully reinstalled the extension."), + [{ + label: localize('ReinstallAction.reloadNow', "Reload Now"), + run: () => this.windowService.reloadWindow() + }] + ); }, error => this.notificationService.error(error)); } } \ No newline at end of file diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensionsUtils.ts b/src/vs/workbench/parts/extensions/electron-browser/extensionsUtils.ts index 42f909ce560..104644f1c7f 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/extensionsUtils.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/extensionsUtils.ts @@ -8,17 +8,15 @@ import * as arrays from 'vs/base/common/arrays'; import { localize } from 'vs/nls'; import { Event, chain, anyEvent, debounceEvent } from 'vs/base/common/event'; -import { onUnexpectedError, canceled } from 'vs/base/common/errors'; +import { onUnexpectedError } from 'vs/base/common/errors'; import { TPromise } from 'vs/base/common/winjs.base'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { IExtensionManagementService, ILocalExtension, IExtensionEnablementService, IExtensionTipsService, LocalExtensionType, IExtensionIdentifier, EnablementState } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { IExtensionManagementService, ILocalExtension, IExtensionEnablementService, IExtensionTipsService, IExtensionIdentifier, EnablementState } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; -import { BetterMergeDisabledNowKey, BetterMergeId, areSameExtensions, adoptToGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { areSameExtensions, adoptToGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { getIdAndVersionFromLocalExtensionId } from 'vs/platform/extensionManagement/node/extensionManagementUtil'; import { Severity, INotificationService } from 'vs/platform/notification/common/notification'; @@ -63,36 +61,37 @@ export class KeymapExtensions implements IWorkbenchContribution { }); } - private promptForDisablingOtherKeymaps(newKeymap: IExtensionStatus, oldKeymaps: IExtensionStatus[]): TPromise { - const message = localize('disableOtherKeymapsConfirmation', "Disable other keymaps ({0}) to avoid conflicts between keybindings?", oldKeymaps.map(k => `'${k.local.manifest.displayName}'`).join(', ')); - const options = [ - localize('yes', "Yes"), - localize('no', "No") - ]; - return this.notificationService.prompt(Severity.Info, message, options) - .then(value => { - const confirmed = value === 0; - const telemetryData: { [key: string]: any; } = { - newKeymap: newKeymap.identifier, - oldKeymaps: oldKeymaps.map(k => k.identifier), - confirmed - }; - /* __GDPR__ - "disableOtherKeymaps" : { - "newKeymap": { "${inline}": [ "${ExtensionIdentifier}" ] }, - "oldKeymaps": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "confirmed" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true } - } - */ - this.telemetryService.publicLog('disableOtherKeymaps', telemetryData); - if (confirmed) { - return TPromise.join(oldKeymaps.map(keymap => { - return this.extensionEnablementService.setEnablement(keymap.local, EnablementState.Disabled); - })); + private promptForDisablingOtherKeymaps(newKeymap: IExtensionStatus, oldKeymaps: IExtensionStatus[]): void { + const onPrompt = (confirmed: boolean) => { + const telemetryData: { [key: string]: any; } = { + newKeymap: newKeymap.identifier, + oldKeymaps: oldKeymaps.map(k => k.identifier), + confirmed + }; + /* __GDPR__ + "disableOtherKeymaps" : { + "newKeymap": { "${inline}": [ "${ExtensionIdentifier}" ] }, + "oldKeymaps": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "confirmed" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true } } - return undefined; - }, error => TPromise.wrapError(canceled())) - .then(() => { /* drop resolved value */ }); + */ + this.telemetryService.publicLog('disableOtherKeymaps', telemetryData); + if (confirmed) { + TPromise.join(oldKeymaps.map(keymap => { + return this.extensionEnablementService.setEnablement(keymap.local, EnablementState.Disabled); + })); + } + }; + + this.notificationService.prompt(Severity.Info, localize('disableOtherKeymapsConfirmation', "Disable other keymaps ({0}) to avoid conflicts between keybindings?", oldKeymaps.map(k => `'${k.local.manifest.displayName}'`).join(', ')), + [{ + label: localize('yes', "Yes"), + run: () => onPrompt(true) + }, { + label: localize('no', "No"), + run: () => onPrompt(false) + }] + ); } dispose(): void { @@ -143,29 +142,3 @@ export function isKeymapExtension(tipsService: IExtensionTipsService, extension: function stripVersion(id: string): string { return getIdAndVersionFromLocalExtensionId(id).id; } - -export class BetterMergeDisabled implements IWorkbenchContribution { - - constructor( - @IStorageService storageService: IStorageService, - @INotificationService notificationService: INotificationService, - @IExtensionService extensionService: IExtensionService, - @IExtensionManagementService extensionManagementService: IExtensionManagementService, - @ITelemetryService telemetryService: ITelemetryService, - ) { - extensionService.whenInstalledExtensionsRegistered().then(() => { - if (storageService.getBoolean(BetterMergeDisabledNowKey, StorageScope.GLOBAL, false)) { - storageService.remove(BetterMergeDisabledNowKey, StorageScope.GLOBAL); - - notificationService.prompt(Severity.Info, localize('betterMergeDisabled', "The Better Merge extension is now built-in, the installed extension was disabled and can be uninstalled."), [localize('uninstall', "Uninstall")]).then(choice => { - if (choice === 0) { - extensionManagementService.getInstalled(LocalExtensionType.User).then(extensions => { - return Promise.all(extensions.filter(e => stripVersion(e.identifier.id) === BetterMergeId) - .map(e => extensionManagementService.uninstall(e, true))); - }); - } - }); - } - }); - } -} diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensionsViewlet.ts b/src/vs/workbench/parts/extensions/electron-browser/extensionsViewlet.ts index 3529fe7d3b3..2b02d88b44c 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/extensionsViewlet.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/extensionsViewlet.ts @@ -12,7 +12,6 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { isPromiseCanceledError, onUnexpectedError, create as createError } from 'vs/base/common/errors'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { Builder, Dimension } from 'vs/base/browser/builder'; import { Event as EventOf, mapEvent, chain } from 'vs/base/common/event'; import { IAction } from 'vs/base/common/actions'; import { domEvent } from 'vs/base/browser/event'; @@ -21,7 +20,7 @@ import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { KeyCode } from 'vs/base/common/keyCodes'; import { IViewlet } from 'vs/workbench/common/viewlet'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; -import { append, $, addStandardDisposableListener, EventType, addClass, removeClass, toggleClass } from 'vs/base/browser/dom'; +import { append, $, addStandardDisposableListener, EventType, addClass, removeClass, toggleClass, Dimension } from 'vs/base/browser/dom'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; @@ -67,65 +66,11 @@ const SearchBuiltInExtensionsContext = new RawContextKey('searchBuiltIn const RecommendedExtensionsContext = new RawContextKey('recommendedExtensions', false); const DefaultRecommendedExtensionsContext = new RawContextKey('defaultRecommendedExtensions', false); -export class ExtensionsViewlet extends PersistentViewsViewlet implements IExtensionsViewlet { - - private onSearchChange: EventOf; - private nonEmptyWorkspaceContextKey: IContextKey; - private searchExtensionsContextKey: IContextKey; - private searchInstalledExtensionsContextKey: IContextKey; - private searchBuiltInExtensionsContextKey: IContextKey; - private recommendedExtensionsContextKey: IContextKey; - private defaultRecommendedExtensionsContextKey: IContextKey; - - private searchDelayer: ThrottledDelayer; - private root: HTMLElement; - - private searchBox: HTMLInputElement; - private extensionsBox: HTMLElement; - private primaryActions: IAction[]; - private secondaryActions: IAction[]; - private disposables: IDisposable[] = []; +export class ExtensionsViewletViewsContribution implements IWorkbenchContribution { constructor( - @IPartService partService: IPartService, - @ITelemetryService telemetryService: ITelemetryService, - @IProgressService private progressService: IProgressService, - @IInstantiationService instantiationService: IInstantiationService, - @IWorkbenchEditorService private editorService: IWorkbenchEditorService, - @IEditorGroupService private editorInputService: IEditorGroupService, - @IExtensionManagementService private extensionManagementService: IExtensionManagementService, - @INotificationService private notificationService: INotificationService, - @IViewletService private viewletService: IViewletService, - @IThemeService themeService: IThemeService, - @IConfigurationService private configurationService: IConfigurationService, - @IStorageService storageService: IStorageService, - @IWorkspaceContextService contextService: IWorkspaceContextService, - @IContextKeyService contextKeyService: IContextKeyService, - @IContextMenuService contextMenuService: IContextMenuService, - @IExtensionService extensionService: IExtensionService ) { - super(VIEWLET_ID, ViewLocation.Extensions, `${VIEWLET_ID}.state`, true, partService, telemetryService, storageService, instantiationService, themeService, contextService, contextKeyService, contextMenuService, extensionService); - this.registerViews(); - this.searchDelayer = new ThrottledDelayer(500); - this.nonEmptyWorkspaceContextKey = NonEmptyWorkspaceContext.bindTo(contextKeyService); - this.searchExtensionsContextKey = SearchExtensionsContext.bindTo(contextKeyService); - this.searchInstalledExtensionsContextKey = SearchInstalledExtensionsContext.bindTo(contextKeyService); - this.searchBuiltInExtensionsContextKey = SearchBuiltInExtensionsContext.bindTo(contextKeyService); - this.recommendedExtensionsContextKey = RecommendedExtensionsContext.bindTo(contextKeyService); - this.defaultRecommendedExtensionsContextKey = DefaultRecommendedExtensionsContext.bindTo(contextKeyService); - this.defaultRecommendedExtensionsContextKey.set(!this.configurationService.getValue(ShowRecommendationsOnlyOnDemandKey)); - this.disposables.push(this.viewletService.onDidViewletOpen(this.onViewletOpen, this, this.disposables)); - - this.configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(AutoUpdateConfigurationKey)) { - this.secondaryActions = null; - this.updateTitleArea(); - } - if (e.affectedKeys.indexOf(ShowRecommendationsOnlyOnDemandKey) > -1) { - this.defaultRecommendedExtensionsContextKey.set(!this.configurationService.getValue(ShowRecommendationsOnlyOnDemandKey)); - } - }, this, this.disposables); } private registerViews(): void { @@ -240,7 +185,7 @@ export class ExtensionsViewlet extends PersistentViewsViewlet implements IExtens private createSearchBuiltInBasicsExtensionsListViewDescriptor(): IViewDescriptor { return { id: 'extensions.builtInBasicsExtensionsList', - name: localize('builtInBasicsExtensions', "Languages"), + name: localize('builtInBasicsExtensions', "Programming Languages"), location: ViewLocation.Extensions, ctor: BuiltInBasicsExtensionsView, when: ContextKeyExpr.has('searchBuiltInExtensions'), @@ -248,10 +193,71 @@ export class ExtensionsViewlet extends PersistentViewsViewlet implements IExtens canToggleVisibility: true }; } +} - async create(parent: Builder): TPromise { - parent.addClass('extensions-viewlet'); - this.root = parent.getHTMLElement(); +export class ExtensionsViewlet extends PersistentViewsViewlet implements IExtensionsViewlet { + + private onSearchChange: EventOf; + private nonEmptyWorkspaceContextKey: IContextKey; + private searchExtensionsContextKey: IContextKey; + private searchInstalledExtensionsContextKey: IContextKey; + private searchBuiltInExtensionsContextKey: IContextKey; + private recommendedExtensionsContextKey: IContextKey; + private defaultRecommendedExtensionsContextKey: IContextKey; + + private searchDelayer: ThrottledDelayer; + private root: HTMLElement; + + private searchBox: HTMLInputElement; + private extensionsBox: HTMLElement; + private primaryActions: IAction[]; + private secondaryActions: IAction[]; + private disposables: IDisposable[] = []; + + constructor( + @IPartService partService: IPartService, + @ITelemetryService telemetryService: ITelemetryService, + @IProgressService private progressService: IProgressService, + @IInstantiationService instantiationService: IInstantiationService, + @IWorkbenchEditorService private editorService: IWorkbenchEditorService, + @IEditorGroupService private editorInputService: IEditorGroupService, + @IExtensionManagementService private extensionManagementService: IExtensionManagementService, + @INotificationService private notificationService: INotificationService, + @IViewletService private viewletService: IViewletService, + @IThemeService themeService: IThemeService, + @IConfigurationService private configurationService: IConfigurationService, + @IStorageService storageService: IStorageService, + @IWorkspaceContextService contextService: IWorkspaceContextService, + @IContextKeyService contextKeyService: IContextKeyService, + @IContextMenuService contextMenuService: IContextMenuService, + @IExtensionService extensionService: IExtensionService + ) { + super(VIEWLET_ID, ViewLocation.Extensions, `${VIEWLET_ID}.state`, true, partService, telemetryService, storageService, instantiationService, themeService, contextService, contextKeyService, contextMenuService, extensionService); + + this.searchDelayer = new ThrottledDelayer(500); + this.nonEmptyWorkspaceContextKey = NonEmptyWorkspaceContext.bindTo(contextKeyService); + this.searchExtensionsContextKey = SearchExtensionsContext.bindTo(contextKeyService); + this.searchInstalledExtensionsContextKey = SearchInstalledExtensionsContext.bindTo(contextKeyService); + this.searchBuiltInExtensionsContextKey = SearchBuiltInExtensionsContext.bindTo(contextKeyService); + this.recommendedExtensionsContextKey = RecommendedExtensionsContext.bindTo(contextKeyService); + this.defaultRecommendedExtensionsContextKey = DefaultRecommendedExtensionsContext.bindTo(contextKeyService); + this.defaultRecommendedExtensionsContextKey.set(!this.configurationService.getValue(ShowRecommendationsOnlyOnDemandKey)); + this.disposables.push(this.viewletService.onDidViewletOpen(this.onViewletOpen, this, this.disposables)); + + this.configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(AutoUpdateConfigurationKey)) { + this.secondaryActions = null; + this.updateTitleArea(); + } + if (e.affectedKeys.indexOf(ShowRecommendationsOnlyOnDemandKey) > -1) { + this.defaultRecommendedExtensionsContextKey.set(!this.configurationService.getValue(ShowRecommendationsOnlyOnDemandKey)); + } + }, this, this.disposables); + } + + async create(parent: HTMLElement): TPromise { + addClass(parent, 'extensions-viewlet'); + this.root = parent; const header = append(this.root, $('.header')); @@ -278,7 +284,7 @@ export class ExtensionsViewlet extends PersistentViewsViewlet implements IExtens this.onSearchChange = mapEvent(onSearchInput, e => e.target.value); - await super.create(new Builder(this.extensionsBox)); + await super.create(this.extensionsBox); const installed = await this.extensionManagementService.getInstalled(LocalExtensionType.User); @@ -377,7 +383,7 @@ export class ExtensionsViewlet extends PersistentViewsViewlet implements IExtens private async doSearch(): TPromise { const value = this.searchBox.value || ''; this.searchExtensionsContextKey.set(!!value); - this.searchInstalledExtensionsContextKey.set(InstalledExtensionsView.isInsalledExtensionsQuery(value)); + this.searchInstalledExtensionsContextKey.set(InstalledExtensionsView.isInstalledExtensionsQuery(value)); this.searchBuiltInExtensionsContextKey.set(ExtensionsListView.isBuiltInExtensionsQuery(value)); this.recommendedExtensionsContextKey.set(ExtensionsListView.isRecommendedExtensionsQuery(value)); this.nonEmptyWorkspaceContextKey.set(this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY); @@ -535,13 +541,14 @@ export class MaliciousExtensionChecker implements IWorkbenchContribution { if (maliciousExtensions.length) { return TPromise.join(maliciousExtensions.map(e => this.extensionsManagementService.uninstall(e, true).then(() => { - return this.notificationService.prompt(Severity.Warning, localize('malicious warning', "We have uninstalled '{0}' which was reported to be problematic.", getGalleryExtensionIdFromLocal(e)), [localize('reloadNow', "Reload Now")]).then(choice => { - if (choice === 0) { - return this.windowService.reloadWindow(); - } - - return TPromise.as(undefined); - }); + this.notificationService.prompt( + Severity.Warning, + localize('malicious warning', "We have uninstalled '{0}' which was reported to be problematic.", getGalleryExtensionIdFromLocal(e)), + [{ + label: localize('reloadNow', "Reload Now"), + run: () => this.windowService.reloadWindow() + }] + ); }))); } else { return TPromise.as(null); diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensionsViews.ts b/src/vs/workbench/parts/extensions/electron-browser/extensionsViews.ts index 07dd776deb6..1065c40b80d 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/extensionsViews.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/extensionsViews.ts @@ -204,7 +204,7 @@ export class ExtensionsListView extends ViewsViewletPanel { let result = await this.extensionsWorkbenchService.queryLocal(); result = result - .filter(e => e.type === LocalExtensionType.User && e.name.toLowerCase().indexOf(value) > -1); + .filter(e => e.type === LocalExtensionType.User && (e.name.toLowerCase().indexOf(value) > -1 || e.displayName.toLowerCase().indexOf(value) > -1)); return new PagedModel(this.sortExtensions(result, options)); } @@ -224,7 +224,7 @@ export class ExtensionsListView extends ViewsViewletPanel { const local = await this.extensionsWorkbenchService.queryLocal(); const result = local .sort((e1, e2) => e1.displayName.localeCompare(e2.displayName)) - .filter(extension => extension.outdated && extension.name.toLowerCase().indexOf(value) > -1); + .filter(extension => extension.outdated && (extension.name.toLowerCase().indexOf(value) > -1 || extension.displayName.toLowerCase().indexOf(value) > -1)); return new PagedModel(this.sortExtensions(result, options)); } @@ -237,7 +237,7 @@ export class ExtensionsListView extends ViewsViewletPanel { const result = local .sort((e1, e2) => e1.displayName.localeCompare(e2.displayName)) - .filter(e => runningExtensions.every(r => !areSameExtensions(r, e)) && e.name.toLowerCase().indexOf(value) > -1); + .filter(e => runningExtensions.every(r => !areSameExtensions(r, e)) && (e.name.toLowerCase().indexOf(value) > -1 || e.displayName.toLowerCase().indexOf(value) > -1)); return new PagedModel(this.sortExtensions(result, options)); } @@ -251,7 +251,7 @@ export class ExtensionsListView extends ViewsViewletPanel { .sort((e1, e2) => e1.displayName.localeCompare(e2.displayName)) .filter(e => e.type === LocalExtensionType.User && (e.enablementState === EnablementState.Enabled || e.enablementState === EnablementState.WorkspaceEnabled) && - e.name.toLowerCase().indexOf(value) > -1 + (e.name.toLowerCase().indexOf(value) > -1 || e.displayName.toLowerCase().indexOf(value) > -1) ); return new PagedModel(this.sortExtensions(result, options)); @@ -559,7 +559,7 @@ export class ExtensionsListView extends ViewsViewletPanel { export class InstalledExtensionsView extends ExtensionsListView { - public static isInsalledExtensionsQuery(query: string): boolean { + public static isInstalledExtensionsQuery(query: string): boolean { return ExtensionsListView.isInstalledExtensionsQuery(query) || ExtensionsListView.isOutdatedExtensionsQuery(query) || ExtensionsListView.isDisabledExtensionsQuery(query) @@ -567,7 +567,7 @@ export class InstalledExtensionsView extends ExtensionsListView { } async show(query: string): TPromise> { - if (InstalledExtensionsView.isInsalledExtensionsQuery(query)) { + if (InstalledExtensionsView.isInstalledExtensionsQuery(query)) { return super.show(query); } let searchInstalledQuery = '@installed'; diff --git a/src/vs/workbench/parts/extensions/electron-browser/media/extensionEditor.css b/src/vs/workbench/parts/extensions/electron-browser/media/extensionEditor.css index b0a1a2e64fa..969dbc34445 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/media/extensionEditor.css +++ b/src/vs/workbench/parts/extensions/electron-browser/media/extensionEditor.css @@ -34,6 +34,7 @@ flex: 1; padding-left: 20px; overflow: hidden; + user-select: text; } .extension-editor > .header > .details > .title { @@ -77,6 +78,7 @@ margin-left: 10px; padding: 0px 4px; border-radius: 4px; + user-select: none; } .extension-editor > .header > .details > .subtitle { diff --git a/src/vs/workbench/parts/extensions/electron-browser/runtimeExtensionsEditor.ts b/src/vs/workbench/parts/extensions/electron-browser/runtimeExtensionsEditor.ts index 331d6a62735..e51d6079969 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/runtimeExtensionsEditor.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/runtimeExtensionsEditor.ts @@ -14,7 +14,6 @@ import { EditorInput } from 'vs/workbench/common/editor'; import pkg from 'vs/platform/node/package'; import { TPromise } from 'vs/base/common/winjs.base'; import { Action, IAction } from 'vs/base/common/actions'; -import { Builder, Dimension } from 'vs/base/browser/builder'; import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IInstantiationService, createDecorator } from 'vs/platform/instantiation/common/instantiation'; @@ -24,7 +23,7 @@ import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/edi import { IExtensionService, IExtensionDescription, IExtensionsStatus, IExtensionHostProfile } from 'vs/workbench/services/extensions/common/extensions'; import { IDelegate, IRenderer } from 'vs/base/browser/ui/list/list'; import { WorkbenchList } from 'vs/platform/list/browser/listService'; -import { append, $, addClass, toggleClass } from 'vs/base/browser/dom'; +import { append, $, addClass, toggleClass, Dimension } from 'vs/base/browser/dom'; import { ActionBar, Separator } from 'vs/base/browser/ui/actionbar/actionbar'; import { dispose, IDisposable } from 'vs/base/common/lifecycle'; import { RunOnceScheduler } from 'vs/base/common/async'; @@ -212,10 +211,8 @@ export class RuntimeExtensionsEditor extends BaseEditor { return result; } - protected createEditor(parent: Builder): void { - const container = parent.getHTMLElement(); - - addClass(container, 'runtime-extensions-editor'); + protected createEditor(parent: HTMLElement): void { + addClass(parent, 'runtime-extensions-editor'); const TEMPLATE_ID = 'runtimeExtensionElementTemplate'; @@ -299,7 +296,7 @@ export class RuntimeExtensionsEditor extends BaseEditor { let syncTime = activationTimes.codeLoadingTime + activationTimes.activateCallTime; data.activationTime.textContent = activationTimes.startup ? `Startup Activation: ${syncTime}ms` : `Activation: ${syncTime}ms`; data.actionbar.context = element; - toggleClass(data.actionbar.getContainer().getHTMLElement(), 'hidden', element.marketplaceInfo && element.marketplaceInfo.type === LocalExtensionType.User && (!element.description.repository || !element.description.repository.url)); + toggleClass(data.actionbar.getContainer(), 'hidden', element.marketplaceInfo && element.marketplaceInfo.type === LocalExtensionType.User && (!element.description.repository || !element.description.repository.url)); let title: string; if (activationTimes.activationEvent === '*') { @@ -307,15 +304,30 @@ export class RuntimeExtensionsEditor extends BaseEditor { } else if (/^workspaceContains:/.test(activationTimes.activationEvent)) { let fileNameOrGlob = activationTimes.activationEvent.substr('workspaceContains:'.length); if (fileNameOrGlob.indexOf('*') >= 0 || fileNameOrGlob.indexOf('?') >= 0) { - title = nls.localize('workspaceContainsGlobActivation', "Activated because a file matching {0} exists in your workspace", fileNameOrGlob); + title = nls.localize({ + key: 'workspaceContainsGlobActivation', + comment: [ + '{0} will be a glob pattern' + ] + }, "Activated because a file matching {0} exists in your workspace", fileNameOrGlob); } else { - title = nls.localize('workspaceContainsFileActivation', "Activated because file {0} exists in your workspace", fileNameOrGlob); + title = nls.localize({ + key: 'workspaceContainsFileActivation', + comment: [ + '{0} will be a file name' + ] + }, "Activated because file {0} exists in your workspace", fileNameOrGlob); } } else if (/^onLanguage:/.test(activationTimes.activationEvent)) { let language = activationTimes.activationEvent.substr('onLanguage:'.length); title = nls.localize('languageActivation', "Activated because you opened a {0} file", language); } else { - title = nls.localize('workspaceGenericActivation', "Activated on {0}", activationTimes.activationEvent); + title = nls.localize({ + key: 'workspaceGenericActivation', + comment: [ + 'The {0} placeholder will be an activation event, like e.g. \'language:typescript\', \'debug\', etc.' + ] + }, "Activated on {0}", activationTimes.activationEvent); } data.activationTime.title = title; if (!isFalsyOrEmpty(element.status.runtimeErrors)) { @@ -362,7 +374,7 @@ export class RuntimeExtensionsEditor extends BaseEditor { } }; - this._list = this._instantiationService.createInstance(WorkbenchList, container, delegate, [renderer], { + this._list = this._instantiationService.createInstance(WorkbenchList, parent, delegate, [renderer], { multipleSelectionSupport: false }) as WorkbenchList; diff --git a/src/vs/workbench/parts/extensions/node/extensionsWorkbenchService.ts b/src/vs/workbench/parts/extensions/node/extensionsWorkbenchService.ts index 77f8b5b09b5..89fc28f9bf0 100644 --- a/src/vs/workbench/parts/extensions/node/extensionsWorkbenchService.ts +++ b/src/vs/workbench/parts/extensions/node/extensionsWorkbenchService.ts @@ -323,36 +323,14 @@ class ExtensionDependencies implements IExtensionDependencies { } } -enum Operation { - Installing, - Updating, - Uninstalling -} - -interface IActiveExtension { - operation: Operation; - extension: Extension; - start: Date; -} - -function toTelemetryEventName(operation: Operation) { - switch (operation) { - case Operation.Installing: return 'extensionGallery:install'; - case Operation.Updating: return 'extensionGallery:update'; - case Operation.Uninstalling: return 'extensionGallery:uninstall'; - } - - return ''; -} - export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService, IURLHandler { private static readonly SyncPeriod = 1000 * 60 * 60 * 12; // 12 hours _serviceBrand: any; private stateProvider: IExtensionStateProvider; - private installing: IActiveExtension[] = []; - private uninstalling: IActiveExtension[] = []; + private installing: Extension[] = []; + private uninstalling: Extension[] = []; private installed: Extension[] = []; private syncDelayer: ThrottledDelayer; private autoUpdateDelayer: ThrottledDelayer; @@ -405,8 +383,8 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService, get local(): IExtension[] { const installing = this.installing - .filter(e => !this.installed.some(installed => installed.id === e.extension.id)) - .map(e => e.extension); + .filter(e => !this.installed.some(installed => installed.id === e.id)) + .map(e => e); return [...this.installed, ...installing]; } @@ -810,35 +788,22 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService, extension.gallery = gallery; - const start = new Date(); - const operation = Operation.Installing; - this.installing.push({ operation, extension, start }); + this.installing.push(extension); this._onChange.fire(); } private onDidInstallExtension(event: DidInstallExtensionEvent): void { const { local, zipPath, error, gallery } = event; - const installingExtension = gallery ? this.installing.filter(e => areSameExtensions(e.extension, gallery.identifier))[0] : null; - const extension: Extension = installingExtension ? installingExtension.extension : zipPath ? new Extension(this.galleryService, this.stateProvider, null, null, this.telemetryService) : null; + const installingExtension = gallery ? this.installing.filter(e => areSameExtensions(e, gallery.identifier))[0] : null; + const extension: Extension = installingExtension ? installingExtension : zipPath ? new Extension(this.galleryService, this.stateProvider, null, null, this.telemetryService) : null; if (extension) { this.installing = installingExtension ? this.installing.filter(e => e !== installingExtension) : this.installing; - if (error) { - if (extension.gallery) { - // Updating extension can be only a gallery extension - const installed = this.installed.filter(e => e.id === extension.id)[0]; - if (installed && installingExtension) { - installingExtension.operation = Operation.Updating; - } - } - } else { + if (!error) { extension.local = local; const installed = this.installed.filter(e => e.id === extension.id)[0]; if (installed) { - if (installingExtension) { - installingExtension.operation = Operation.Updating; - } installed.local = local; } else { this.installed.push(extension); @@ -846,7 +811,7 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService, } if (extension.gallery) { // Report telemetry only for gallery extensions - this.reportTelemetry(installingExtension, error); + this.reportExtensionRecommendationsTelemetry(installingExtension); } } this._onChange.fire(); @@ -861,10 +826,8 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService, return; } - const start = new Date(); - const operation = Operation.Uninstalling; - const uninstalling = this.uninstalling.filter(e => e.extension.local.identifier.id === id)[0] || { id, operation, extension, start }; - this.uninstalling = [uninstalling, ...this.uninstalling.filter(e => e.extension.local.identifier.id !== id)]; + const uninstalling = this.uninstalling.filter(e => e.local.identifier.id === id)[0] || extension; + this.uninstalling = [uninstalling, ...this.uninstalling.filter(e => e.local.identifier.id !== id)]; this._onChange.fire(); } @@ -875,16 +838,12 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService, this.installed = this.installed.filter(e => e.local.identifier.id !== id); } - const uninstalling = this.uninstalling.filter(e => e.extension.local.identifier.id === id)[0]; - this.uninstalling = this.uninstalling.filter(e => e.extension.local.identifier.id !== id); + const uninstalling = this.uninstalling.filter(e => e.local.identifier.id === id)[0]; + this.uninstalling = this.uninstalling.filter(e => e.local.identifier.id !== id); if (!uninstalling) { return; } - if (!error) { - this.reportTelemetry(uninstalling); - } - this._onChange.fire(); } @@ -900,11 +859,11 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService, } private getExtensionState(extension: Extension): ExtensionState { - if (extension.gallery && this.installing.some(e => e.extension.gallery && areSameExtensions(e.extension.gallery.identifier, extension.gallery.identifier))) { + if (extension.gallery && this.installing.some(e => e.gallery && areSameExtensions(e.gallery.identifier, extension.gallery.identifier))) { return ExtensionState.Installing; } - if (this.uninstalling.some(e => e.extension.id === extension.id)) { + if (this.uninstalling.some(e => e.id === extension.id)) { return ExtensionState.Uninstalling; } @@ -912,46 +871,22 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService, return local ? ExtensionState.Installed : ExtensionState.Uninstalled; } - private reportTelemetry(active: IActiveExtension, errorcode?: string): void { - const data = active.extension.telemetryData; - const duration = new Date().getTime() - active.start.getTime(); - const eventName = toTelemetryEventName(active.operation); + private reportExtensionRecommendationsTelemetry(extension: Extension): void { const extRecommendations = this.extensionTipsService.getAllRecommendationsWithReason() || {}; - const recommendationsData = extRecommendations[active.extension.id.toLowerCase()] ? { recommendationReason: extRecommendations[active.extension.id.toLowerCase()].reasonId } : {}; - /* __GDPR__ - "extensionGallery:install" : { - "success": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, - "duration" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, - "errorcode": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, - "recommendationReason": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, - "${include}": [ - "${GalleryExtensionTelemetryData}" - ] - } - */ - /* __GDPR__ - "extensionGallery:update" : { - "success": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, - "duration" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, - "errorcode": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, - "recommendationReason": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, - "${include}": [ - "${GalleryExtensionTelemetryData}" - ] - } - */ - /* __GDPR__ - "extensionGallery:uninstall" : { - "success": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, - "duration" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, - "errorcode": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, - "recommendationReason": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, - "${include}": [ - "${GalleryExtensionTelemetryData}" - ] - } - */ - this.telemetryService.publicLog(eventName, assign(data, { success: !errorcode, duration, errorcode }, recommendationsData)); + const recommendationReason = extRecommendations[extension.id.toLowerCase()]; + if (recommendationReason) { + const recommendationsData = { recommendationReason: recommendationReason.reasonId }; + const data = extension.telemetryData; + /* __GDPR__ + "extensionGallery:install:recommendations" : { + "recommendationReason": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "${include}": [ + "${GalleryExtensionTelemetryData}" + ] + } + */ + this.telemetryService.publicLog('extensionGallery:install:recommendations', assign(data, recommendationsData)); + } } private onError(err: any): void { @@ -1003,17 +938,14 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService, return this.windowService.show().then(() => { return this.open(extension).then(() => { - const message = nls.localize('installConfirmation', "Would you like to install the '{0}' extension?", extension.displayName, extension.publisher); - const options = [ - nls.localize('install', "Install") - ]; - return this.notificationService.prompt(Severity.Info, message, options).then(value => { - if (value === 0) { - return this.install(extension); - } - - return TPromise.as(null); - }); + this.notificationService.prompt( + Severity.Info, + nls.localize('installConfirmation', "Would you like to install the '{0}' extension?", extension.displayName, extension.publisher), + [{ + label: nls.localize('install', "Install"), + run: () => this.install(extension).done(undefined, error => this.onError(error)) + }] + ); }); }); }); diff --git a/src/vs/workbench/parts/extensions/test/electron-browser/extensionsTipsService.test.ts b/src/vs/workbench/parts/extensions/test/electron-browser/extensionsTipsService.test.ts index fd2f7e41c28..e99f9a52370 100644 --- a/src/vs/workbench/parts/extensions/test/electron-browser/extensionsTipsService.test.ts +++ b/src/vs/workbench/parts/extensions/test/electron-browser/extensionsTipsService.test.ts @@ -23,12 +23,13 @@ import { Emitter } from 'vs/base/common/event'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { TestTextResourceConfigurationService, TestContextService, TestLifecycleService, TestEnvironmentService, TestNotificationService } from 'vs/workbench/test/workbenchTestServices'; +import { TestTextResourceConfigurationService, TestContextService, TestLifecycleService, TestEnvironmentService, TestStorageService } from 'vs/workbench/test/workbenchTestServices'; +import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import URI from 'vs/base/common/uri'; import { testWorkspace } from 'vs/platform/workspace/test/common/testWorkspace'; import { IFileService } from 'vs/platform/files/common/files'; -import { FileService } from 'vs/workbench/services/files/node/fileService'; +import { FileService } from 'vs/workbench/services/files/electron-browser/fileService'; import * as extfs from 'vs/base/node/extfs'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { IPager } from 'vs/base/common/paging'; @@ -45,7 +46,7 @@ import product from 'vs/platform/node/product'; import { ITextModel } from 'vs/editor/common/model'; import { IModelService } from 'vs/editor/common/services/modelService'; import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; -import { INotificationService } from 'vs/platform/notification/common/notification'; +import { INotificationService, Severity, IPromptChoice } from 'vs/platform/notification/common/notification'; import { URLService } from 'vs/platform/url/common/urlService'; const mockExtensionGallery: IGalleryExtension[] = [ @@ -223,9 +224,9 @@ suite('ExtensionsTipsService Test', () => { prompted = false; class TestNotificationService2 extends TestNotificationService { - public prompt() { + public prompt(severity: Severity, message: string, choices: IPromptChoice[], onCancel?: () => void) { prompted = true; - return TPromise.as(3); + return null; } } @@ -265,7 +266,7 @@ suite('ExtensionsTipsService Test', () => { const myWorkspace = testWorkspace(URI.from({ scheme: 'file', path: folderDir })); workspaceService = new TestContextService(myWorkspace); instantiationService.stub(IWorkspaceContextService, workspaceService); - instantiationService.stub(IFileService, new FileService(workspaceService, TestEnvironmentService, new TestTextResourceConfigurationService(), new TestConfigurationService(), new TestLifecycleService(), { disableWatcher: true })); + instantiationService.stub(IFileService, new FileService(workspaceService, TestEnvironmentService, new TestTextResourceConfigurationService(), new TestConfigurationService(), new TestLifecycleService(), new TestStorageService(), new TestNotificationService(), { disableWatcher: true })); }); } diff --git a/src/vs/workbench/parts/feedback/electron-browser/feedback.ts b/src/vs/workbench/parts/feedback/electron-browser/feedback.ts index 4ab3841d55e..eb1e969e4a8 100644 --- a/src/vs/workbench/parts/feedback/electron-browser/feedback.ts +++ b/src/vs/workbench/parts/feedback/electron-browser/feedback.ts @@ -96,8 +96,8 @@ export class FeedbackDropdown extends Dropdown { } }); - this.element.addClass('send-feedback'); - this.element.title(nls.localize('sendFeedback', "Tweet Feedback")); + dom.addClass(this.element, 'send-feedback'); + this.element.title = nls.localize('sendFeedback', "Tweet Feedback"); this.feedbackService = options.feedbackService; @@ -118,7 +118,7 @@ export class FeedbackDropdown extends Dropdown { } protected getAnchor(): HTMLElement | IAnchor { - const res = dom.getDomNodePagePosition(this.element.getHTMLElement()); + const res = dom.getDomNodePagePosition(this.element); return { x: res.left, @@ -249,7 +249,7 @@ export class FeedbackDropdown extends Dropdown { $('label').attr('for', 'hide-button').text(nls.localize('showFeedback', "Show Feedback Smiley in Status Bar")).appendTo($hideButtonContainer); - this.sendButton = new Button($buttons); + this.sendButton = new Button($buttons.getHTMLElement()); this.sendButton.enabled = false; this.sendButton.label = nls.localize('tweet', "Tweet"); this.$sendButton = new Builder(this.sendButton.element); diff --git a/src/vs/workbench/parts/feedback/electron-browser/feedbackStatusbarItem.ts b/src/vs/workbench/parts/feedback/electron-browser/feedbackStatusbarItem.ts index 9a233504390..1a659b36d9a 100644 --- a/src/vs/workbench/parts/feedback/electron-browser/feedbackStatusbarItem.ts +++ b/src/vs/workbench/parts/feedback/electron-browser/feedbackStatusbarItem.ts @@ -95,7 +95,7 @@ export class FeedbackStatusbarItem extends Themable implements IStatusbarItem { super.updateStyles(); if (this.dropdown) { - this.dropdown.label.style('background-color', this.getColor(this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY ? STATUS_BAR_FOREGROUND : STATUS_BAR_NO_FOLDER_FOREGROUND)); + $(this.dropdown.label).style('background-color', this.getColor(this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY ? STATUS_BAR_FOREGROUND : STATUS_BAR_NO_FOLDER_FOREGROUND)); } } diff --git a/src/vs/workbench/parts/files/browser/editors/binaryFileEditor.ts b/src/vs/workbench/parts/files/browser/editors/binaryFileEditor.ts index 346898902ab..89fe412b1ac 100644 --- a/src/vs/workbench/parts/files/browser/editors/binaryFileEditor.ts +++ b/src/vs/workbench/parts/files/browser/editors/binaryFileEditor.ts @@ -6,10 +6,15 @@ import * as nls from 'vs/nls'; import { BaseBinaryResourceEditor } from 'vs/workbench/browser/parts/editor/binaryEditor'; -import { BINARY_FILE_EDITOR_ID } from 'vs/workbench/parts/files/common/files'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IWindowsService } from 'vs/platform/windows/common/windows'; +import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { EditorInput, EditorOptions } from 'vs/workbench/common/editor'; +import { onUnexpectedError } from 'vs/base/common/errors'; +import { FileEditorInput } from 'vs/workbench/parts/files/common/editors/fileEditorInput'; +import URI from 'vs/base/common/uri'; +import { BINARY_FILE_EDITOR_ID } from 'vs/workbench/parts/files/common/files'; /** * An implementation of editor for binary files like images. @@ -21,12 +26,38 @@ export class BinaryFileEditor extends BaseBinaryResourceEditor { constructor( @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, - @IWindowsService windowsService: IWindowsService + @IWindowsService private windowsService: IWindowsService, + @IWorkbenchEditorService private editorService: IWorkbenchEditorService ) { - super(BinaryFileEditor.ID, telemetryService, themeService, windowsService); + super( + BinaryFileEditor.ID, + { + openInternal: (input, options) => this.openInternal(input, options), + openExternal: resource => this.openExternal(resource) + }, + telemetryService, + themeService + ); + } + + private openInternal(input: EditorInput, options: EditorOptions): void { + if (input instanceof FileEditorInput) { + input.setForceOpenAsText(); + this.editorService.openEditor(input, options, this.position).done(null, onUnexpectedError); + } + } + + private openExternal(resource: URI): void { + this.windowsService.openExternal(resource.toString()).then(didOpen => { + if (!didOpen) { + return this.windowsService.showItemInFolder(resource.fsPath); + } + + return void 0; + }); } public getTitle(): string { return this.input ? this.input.getName() : nls.localize('binaryFileEditor', "Binary File Viewer"); } -} \ No newline at end of file +} diff --git a/src/vs/workbench/parts/files/browser/editors/fileEditorTracker.ts b/src/vs/workbench/parts/files/browser/editors/fileEditorTracker.ts index 899757c4082..98763468408 100644 --- a/src/vs/workbench/parts/files/browser/editors/fileEditorTracker.ts +++ b/src/vs/workbench/parts/files/browser/editors/fileEditorTracker.ts @@ -11,9 +11,8 @@ import URI from 'vs/base/common/uri'; import * as paths from 'vs/base/common/paths'; import { IEditorViewState } from 'vs/editor/common/editorCommon'; import { toResource, SideBySideEditorInput, IEditorGroup, IWorkbenchEditorConfiguration } from 'vs/workbench/common/editor'; -import { BINARY_FILE_EDITOR_ID } from 'vs/workbench/parts/files/common/files'; import { ITextFileService, ITextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles'; -import { FileOperationEvent, FileOperation, IFileService, FileChangeType, FileChangesEvent, indexOf } from 'vs/platform/files/common/files'; +import { FileOperationEvent, FileOperation, IFileService, FileChangeType, FileChangesEvent } from 'vs/platform/files/common/files'; import { FileEditorInput } from 'vs/workbench/parts/files/common/editors/fileEditorInput'; import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService'; import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; @@ -28,6 +27,8 @@ import { ResourceMap } from 'vs/base/common/map'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; import { SideBySideEditor } from 'vs/workbench/browser/parts/editor/sideBySideEditor'; +import { IWindowService } from 'vs/platform/windows/common/windows'; +import { BINARY_FILE_EDITOR_ID } from 'vs/workbench/parts/files/common/files'; export class FileEditorTracker implements IWorkbenchContribution { @@ -46,6 +47,7 @@ export class FileEditorTracker implements IWorkbenchContribution { @IEnvironmentService private environmentService: IEnvironmentService, @IConfigurationService private configurationService: IConfigurationService, @IWorkspaceContextService private contextService: IWorkspaceContextService, + @IWindowService private windowService: IWindowService ) { this.toUnbind = []; this.modelLoadQueue = new ResourceQueue(); @@ -67,6 +69,9 @@ export class FileEditorTracker implements IWorkbenchContribution { // Editor changing this.toUnbind.push(this.editorGroupService.onEditorsChanged(() => this.onEditorsChanged())); + // Update visible editors when focus is gained + this.toUnbind.push(this.windowService.onDidChangeFocus(e => this.onWindowFocusChange(e))); + // Lifecycle this.lifecycleService.onShutdown(this.dispose, this); @@ -82,6 +87,24 @@ export class FileEditorTracker implements IWorkbenchContribution { } } + private onWindowFocusChange(focused: boolean): void { + if (focused) { + // the window got focus and we use this as a hint that files might have been changed outside + // of this window. since file events can be unreliable, we queue a load for models that + // are visible in any editor. since this is a fast operation in the case nothing has changed, + // we tolerate the additional work. + distinct( + this.editorService.getVisibleEditors() + .map(editor => { + const resource = toResource(editor.input, { supportSideBySide: true }); + return resource ? this.textFileService.models.get(resource) : void 0; + }) + .filter(model => model && !model.isDirty()), + m => m.getResource().toString() + ).forEach(model => this.queueModelLoad(model)); + } + } + // Note: there is some duplication with the other file event handler below. Since we cannot always rely on the disk events // carrying all necessary data in all environments, we also use the file operation events to make sure operations are handled. // In any case there is no guarantee if the local event is fired first or the disk one. Thus, code must handle the case @@ -208,7 +231,7 @@ export class FileEditorTracker implements IWorkbenchContribution { if (oldResource.toString() === resource.toString()) { reopenFileResource = newResource; // file got moved } else { - const index = indexOf(resource.path, oldResource.path, !isLinux /* ignorecase */); + const index = this.getIndexOfPath(resource.path, oldResource.path); reopenFileResource = newResource.with({ path: paths.join(newResource.path, resource.path.substr(index + oldResource.path.length + 1)) }); // parent folder got moved } @@ -229,6 +252,23 @@ export class FileEditorTracker implements IWorkbenchContribution { }); } + private getIndexOfPath(path: string, candidate: string): number { + if (candidate.length > path.length) { + return -1; + } + + if (path === candidate) { + return 0; + } + + if (!isLinux /* ignore case */) { + path = path.toLowerCase(); + candidate = candidate.toLowerCase(); + } + + return path.indexOf(candidate); + } + private getViewStateFor(resource: URI, group: IEditorGroup): IEditorViewState | undefined { const stacks = this.editorGroupService.getStacksModel(); const editors = this.editorService.getVisibleEditors(); diff --git a/src/vs/workbench/parts/files/browser/editors/textFileEditor.ts b/src/vs/workbench/parts/files/browser/editors/textFileEditor.ts index 05f2bfad02c..751b4350f5c 100644 --- a/src/vs/workbench/parts/files/browser/editors/textFileEditor.ts +++ b/src/vs/workbench/parts/files/browser/editors/textFileEditor.ts @@ -11,7 +11,7 @@ import { toErrorMessage } from 'vs/base/common/errorMessage'; import * as types from 'vs/base/common/types'; import * as paths from 'vs/base/common/paths'; import { Action } from 'vs/base/common/actions'; -import { VIEWLET_ID, TEXT_FILE_EDITOR_ID, IExplorerViewlet } from 'vs/workbench/parts/files/common/files'; +import { VIEWLET_ID, IExplorerViewlet, TEXT_FILE_EDITOR_ID } from 'vs/workbench/parts/files/common/files'; import { ITextFileEditorModel, ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { BaseTextEditor } from 'vs/workbench/browser/parts/editor/textEditor'; import { EditorOptions, TextEditorOptions, IEditorCloseEvent } from 'vs/workbench/common/editor'; @@ -24,7 +24,7 @@ import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace import { IStorageService } from 'vs/platform/storage/common/storage'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/resourceConfiguration'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IPreferencesService } from 'vs/workbench/parts/preferences/common/preferences'; +import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; import { PreferencesEditor } from 'vs/workbench/parts/preferences/browser/preferencesEditor'; import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IThemeService } from 'vs/platform/theme/common/themeService'; @@ -171,18 +171,18 @@ export class TextFileEditor extends BaseTextEditor { } if ((error).fileOperationResult === FileOperationResult.FILE_EXCEED_MEMORY_LIMIT) { - let memoryLimit = this.configurationService.getValue(null, 'files.maxMemoryForLargeFilesMB') | 4096; + let memoryLimit = Math.max(2048, +this.configurationService.getValue(null, 'files.maxMemoryForLargeFilesMB') || 4096); return TPromise.wrapError(errors.create(toErrorMessage(error), { actions: [ - new Action('workbench.window.action.relaunchWithIncreasedMemoryLimit', nls.localize('relaunchWithIncreasedMemoryLimit', "Relaunch"), null, true, () => { + new Action('workbench.window.action.relaunchWithIncreasedMemoryLimit', nls.localize('relaunchWithIncreasedMemoryLimit', "Restart with {0} MB", memoryLimit), null, true, () => { return this.windowsService.relaunch({ addArgs: [ `--max-memory=${memoryLimit}` ] }); }), - new Action('workbench.window.action.configureMemoryLimit', nls.localize('configureMemoryLimit', 'Configure'), null, true, () => { + new Action('workbench.window.action.configureMemoryLimit', nls.localize('configureMemoryLimit', 'Configure Memory Limit'), null, true, () => { return this.preferencesService.openGlobalSettings().then(editor => { if (editor instanceof PreferencesEditor) { editor.focusSearch('files.maxMemoryForLargeFilesMB'); @@ -260,4 +260,4 @@ export class TextFileEditor extends BaseTextEditor { this.saveTextEditorViewState(input.getResource()); } } -} \ No newline at end of file +} diff --git a/src/vs/workbench/parts/files/browser/files.ts b/src/vs/workbench/parts/files/browser/files.ts index 2665af86fe9..e1c25edab60 100644 --- a/src/vs/workbench/parts/files/browser/files.ts +++ b/src/vs/workbench/parts/files/browser/files.ts @@ -12,7 +12,6 @@ import { ExplorerItem, OpenEditor } from 'vs/workbench/parts/files/common/explor import { toResource } from 'vs/workbench/common/editor'; import { Tree } from 'vs/base/parts/tree/browser/treeImpl'; import { List } from 'vs/base/browser/ui/list/listWidget'; -import { IFileStat } from 'vs/platform/files/common/files'; // Commands can get exeucted from a command pallete, from a context menu or from some list using a keybinding // To cover all these cases we need to properly compute the resource on which the command is being executed @@ -23,7 +22,16 @@ export function getResourceForCommand(resource: URI | object, listService: IList let list = listService.lastFocusedList; if (list && list.isDOMFocused()) { - const focus = list.getFocus(); + let focus: any; + if (list instanceof List) { + const focused = list.getFocusedElements(); + if (focused.length) { + focus = focused[0]; + } + } else { + focus = list.getFocus(); + } + if (focus instanceof ExplorerItem) { return focus.resource; } else if (focus instanceof OpenEditor) { @@ -39,27 +47,25 @@ export function getMultiSelectedResources(resource: URI | object, listService: I if (list && list.isDOMFocused()) { // Explorer if (list instanceof Tree) { - const focus: IFileStat = list.getFocus(); + const selection = list.getSelection().map((fs: ExplorerItem) => fs.resource); + const focus = list.getFocus(); + const mainUriStr = URI.isUri(resource) ? resource.toString() : focus instanceof ExplorerItem ? focus.resource.toString() : undefined; // If the resource is passed it has to be a part of the returned context. - if (focus && (!URI.isUri(resource) || focus.resource.toString() === resource.toString())) { - const selection = list.getSelection(); - // We only respect the selection if it contains the focused element. - if (selection && selection.indexOf(focus) >= 0) { - return selection.map(fs => fs.resource); - } + // We only respect the selection if it contains the focused element. + if (selection.some(s => s.toString() === mainUriStr)) { + return selection; } } // Open editors view if (list instanceof List) { - const focus = list.getFocusedElements(); - // If the resource is passed it has to be a part of the returned context. - if (focus.length && (!URI.isUri(resource) || (focus[0] instanceof OpenEditor && focus[0].getResource().toString() === resource.toString()))) { - const selection = list.getSelectedElements(); - // We only respect the selection if it contains the focused element. - if (selection && selection.indexOf(focus[0]) >= 0) { - return selection.filter(s => s instanceof OpenEditor).map((oe: OpenEditor) => oe.getResource()); - } + const selection = list.getSelectedElements().filter(s => s instanceof OpenEditor).map((oe: OpenEditor) => oe.getResource()); + const focusedElements = list.getFocusedElements(); + const focus = focusedElements.length ? focusedElements[0] : undefined; + const mainUriStr = URI.isUri(resource) ? resource.toString() : (focus instanceof OpenEditor) ? focus.getResource().toString() : undefined; + // We only respect the selection if it contains the main element. + if (selection.some(s => s.toString() === mainUriStr)) { + return selection; } } } diff --git a/src/vs/workbench/parts/files/common/editors/fileEditorInput.ts b/src/vs/workbench/parts/files/common/editors/fileEditorInput.ts index 5c353824875..fa44017d7f3 100644 --- a/src/vs/workbench/parts/files/common/editors/fileEditorInput.ts +++ b/src/vs/workbench/parts/files/common/editors/fileEditorInput.ts @@ -15,7 +15,6 @@ import { EncodingMode, ConfirmResult, EditorInput, IFileEditorInput, ITextEditor import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; import { BinaryEditorModel } from 'vs/workbench/common/editor/binaryEditorModel'; import { FileOperationError, FileOperationResult } from 'vs/platform/files/common/files'; -import { BINARY_FILE_EDITOR_ID, TEXT_FILE_EDITOR_ID, FILE_EDITOR_INPUT_ID } from 'vs/workbench/parts/files/common/files'; import { ITextFileService, AutoSaveMode, ModelState, TextFileModelChangeEvent } from 'vs/workbench/services/textfile/common/textfiles'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -25,12 +24,15 @@ import { Verbosity, IRevertOptions } from 'vs/platform/editor/common/editor'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { IHashService } from 'vs/workbench/services/hash/common/hashService'; +import { FILE_EDITOR_INPUT_ID, TEXT_FILE_EDITOR_ID, BINARY_FILE_EDITOR_ID } from 'vs/workbench/parts/files/common/files'; /** * A file editor input is the input type for the file editor of file system resources. */ export class FileEditorInput extends EditorInput implements IFileEditorInput { + private preferredEncoding: string; private forceOpenAsBinary: boolean; + private forceOpenAsText: boolean; private textModelReference: TPromise>; private name: string; private toUnbind: IDisposable[]; @@ -40,7 +42,7 @@ export class FileEditorInput extends EditorInput implements IFileEditorInput { */ constructor( private resource: URI, - private preferredEncoding: string, + preferredEncoding: string, @IInstantiationService private instantiationService: IInstantiationService, @IWorkspaceContextService private contextService: IWorkspaceContextService, @ITextFileService private textFileService: ITextFileService, @@ -52,6 +54,8 @@ export class FileEditorInput extends EditorInput implements IFileEditorInput { this.toUnbind = []; + this.setPreferredEncoding(preferredEncoding); + this.registerListeners(); } @@ -83,6 +87,10 @@ export class FileEditorInput extends EditorInput implements IFileEditorInput { public setPreferredEncoding(encoding: string): void { this.preferredEncoding = encoding; + + if (encoding) { + this.forceOpenAsText = true; // encoding is a good hint to open the file as text + } } public getEncoding(): string { @@ -107,8 +115,14 @@ export class FileEditorInput extends EditorInput implements IFileEditorInput { } } + public setForceOpenAsText(): void { + this.forceOpenAsText = true; + this.forceOpenAsBinary = false; + } + public setForceOpenAsBinary(): void { this.forceOpenAsBinary = true; + this.forceOpenAsText = false; } public getTypeId(): string { @@ -234,11 +248,17 @@ export class FileEditorInput extends EditorInput implements IFileEditorInput { // Resolve as binary if (this.forceOpenAsBinary) { - return this.resolveAsBinary(); + return this.doResolveAsBinary(); } // Resolve as text - return this.textFileService.models.loadOrCreate(this.resource, { encoding: this.preferredEncoding, reload: refresh }).then(model => { + return this.doResolveAsText(refresh); + } + + private doResolveAsText(reload?: boolean): TPromise { + + // Resolve as text + return this.textFileService.models.loadOrCreate(this.resource, { encoding: this.preferredEncoding, reload, allowBinary: this.forceOpenAsText }).then(model => { // This is a bit ugly, because we first resolve the model and then resolve a model reference. the reason being that binary // or very large files do not resolve to a text file model but should be opened as binary files without text. First calling into @@ -253,7 +273,7 @@ export class FileEditorInput extends EditorInput implements IFileEditorInput { // In case of an error that indicates that the file is binary or too large, just return with the binary editor model if ((error).fileOperationResult === FileOperationResult.FILE_IS_BINARY || (error).fileOperationResult === FileOperationResult.FILE_TOO_LARGE) { - return this.resolveAsBinary(); + return this.doResolveAsBinary(); } // Bubble any other error up @@ -261,7 +281,7 @@ export class FileEditorInput extends EditorInput implements IFileEditorInput { }); } - private resolveAsBinary(): TPromise { + private doResolveAsBinary(): TPromise { return this.instantiationService.createInstance(BinaryEditorModel, this.resource, this.getName()).load().then(m => m as BinaryEditorModel); } @@ -306,4 +326,4 @@ export class FileEditorInput extends EditorInput implements IFileEditorInput { return false; } -} \ No newline at end of file +} diff --git a/src/vs/workbench/parts/files/common/explorerModel.ts b/src/vs/workbench/parts/files/common/explorerModel.ts index 4634e685b8b..df76d082ac3 100644 --- a/src/vs/workbench/parts/files/common/explorerModel.ts +++ b/src/vs/workbench/parts/files/common/explorerModel.ts @@ -17,7 +17,7 @@ import { IEditorGroup, toResource, IEditorIdentifier } from 'vs/workbench/common import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { getPathLabel } from 'vs/base/common/labels'; import { Schemas } from 'vs/base/common/network'; -import { startsWith, beginsWithIgnoreCase } from 'vs/base/common/strings'; +import { startsWith, startsWithIgnoreCase, equalsIgnoreCase } from 'vs/base/common/strings'; export class Model { @@ -77,7 +77,7 @@ export class ExplorerItem { public etag: string; private _isDirectory: boolean; private _isSymbolicLink: boolean; - private children: { [name: string]: ExplorerItem }; + private children: Map; public parent: ExplorerItem; public isDirectoryResolved: boolean; @@ -109,7 +109,7 @@ export class ExplorerItem { if (value !== this._isDirectory) { this._isDirectory = value; if (this._isDirectory) { - this.children = Object.create(null); + this.children = new Map(); } else { this.children = undefined; } @@ -177,6 +177,7 @@ export class ExplorerItem { local.isDirectory = disk.isDirectory; local.mtime = disk.mtime; local.isDirectoryResolved = disk.isDirectoryResolved; + local._isSymbolicLink = disk.isSymbolicLink; // Merge Children if resolved if (mergingDirectories && disk.isDirectoryResolved) { @@ -184,18 +185,16 @@ export class ExplorerItem { // Map resource => stat const oldLocalChildren = new ResourceMap(); if (local.children) { - for (let name in local.children) { - const child = local.children[name]; + local.children.forEach(child => { oldLocalChildren.set(child.resource, child); - } + }); } // Clear current children - local.children = Object.create(null); + local.children = new Map(); // Merge received children - for (let name in disk.children) { - const diskChild = disk.children[name]; + disk.children.forEach(diskChild => { const formerLocalChild = oldLocalChildren.get(diskChild.resource); // Existing child: merge if (formerLocalChild) { @@ -209,7 +208,7 @@ export class ExplorerItem { diskChild.parent = local; local.addChild(diskChild); } - } + }); } } @@ -222,7 +221,7 @@ export class ExplorerItem { child.parent = this; child.updateResource(false); - this.children[this.getPlatformAwareName(child.name)] = child; + this.children.set(this.getPlatformAwareName(child.name), child); } public getChild(name: string): ExplorerItem { @@ -230,7 +229,7 @@ export class ExplorerItem { return undefined; } - return this.children[this.getPlatformAwareName(name)]; + return this.children.get(this.getPlatformAwareName(name)); } /** @@ -241,7 +240,20 @@ export class ExplorerItem { return undefined; } - return Object.keys(this.children).map(name => this.children[name]); + const items: ExplorerItem[] = []; + this.children.forEach(child => { + items.push(child); + }); + + return items; + } + + public getChildrenCount(): number { + if (!this.children) { + return 0; + } + + return this.children.size; } public getChildrenNames(): string[] { @@ -249,18 +261,23 @@ export class ExplorerItem { return []; } - return Object.keys(this.children); + const names: string[] = []; + this.children.forEach(child => { + names.push(child.name); + }); + + return names; } /** * Removes a child element from this folder. */ public removeChild(child: ExplorerItem): void { - delete this.children[this.getPlatformAwareName(child.name)]; + this.children.delete(this.getPlatformAwareName(child.name)); } private getPlatformAwareName(name: string): string { - return isLinux ? name : name.toLowerCase(); + return (isLinux || !name) ? name : name.toLowerCase(); } /** @@ -288,9 +305,9 @@ export class ExplorerItem { if (recursive) { if (this.isDirectory && this.children) { - for (let name in this.children) { - this.children[name].updateResource(true); - } + this.children.forEach(child => { + child.updateResource(true); + }); } } } @@ -316,7 +333,7 @@ export class ExplorerItem { public find(resource: URI): ExplorerItem { // Return if path found if (resource && this.resource.scheme === resource.scheme && this.resource.authority === resource.authority && - (isLinux ? startsWith(resource.path, this.resource.path) : beginsWithIgnoreCase(resource.path, this.resource.path)) + (isLinux ? startsWith(resource.path, this.resource.path) : startsWithIgnoreCase(resource.path, this.resource.path)) ) { return this.findByPath(resource.path, this.resource.path.length); } @@ -325,7 +342,10 @@ export class ExplorerItem { } private findByPath(path: string, index: number): ExplorerItem { - if (paths.isEqual(this.resource.path, path, !isLinux)) { + if (this.resource.path === path) { + return this; + } + if (!isLinux && equalsIgnoreCase(this.resource.path, path)) { return this; } @@ -343,7 +363,7 @@ export class ExplorerItem { // The name to search is between two separators const name = path.substring(index, indexOfNextSep); - const child = this.children[this.getPlatformAwareName(name)]; + const child = this.children.get(this.getPlatformAwareName(name)); if (child) { // We found a child with the given name, search inside it @@ -358,13 +378,14 @@ export class ExplorerItem { /* A helper that can be used to show a placeholder when creating a new stat */ export class NewStatPlaceholder extends ExplorerItem { + public static NAME = ''; private static ID = 0; private id: number; private directoryPlaceholder: boolean; constructor(isDirectory: boolean, root: ExplorerItem) { - super(URI.file(''), root, false, false, ''); + super(URI.file(''), root, false, false, NewStatPlaceholder.NAME); this.id = NewStatPlaceholder.ID++; this.isDirectoryResolved = isDirectory; diff --git a/src/vs/workbench/parts/files/common/files.ts b/src/vs/workbench/parts/files/common/files.ts index 1c492a3a555..1ddc3245d5f 100644 --- a/src/vs/workbench/parts/files/common/files.ts +++ b/src/vs/workbench/parts/files/common/files.ts @@ -59,20 +59,26 @@ export const FilesExplorerFocusCondition = ContextKeyExpr.and(ContextKeyExpr.has export const ExplorerFocusCondition = ContextKeyExpr.and(ContextKeyExpr.has(explorerViewletVisibleId), ContextKeyExpr.has(explorerViewletFocusId), ContextKeyExpr.not(InputFocusedContextKey)); /** - * File editor input id. + * Preferences editor id. */ -export const FILE_EDITOR_INPUT_ID = 'workbench.editors.files.fileEditorInput'; +export const PREFERENCES_EDITOR_ID = 'workbench.editor.preferencesEditor'; /** * Text file editor id. */ export const TEXT_FILE_EDITOR_ID = 'workbench.editors.files.textFileEditor'; +/** + * File editor input id. + */ +export const FILE_EDITOR_INPUT_ID = 'workbench.editors.files.fileEditorInput'; + /** * Binary file editor id. */ export const BINARY_FILE_EDITOR_ID = 'workbench.editors.files.binaryFileEditor'; + export interface IFilesConfiguration extends IFilesConfiguration, IWorkbenchEditorConfiguration { explorer: { openEditors: { @@ -151,7 +157,7 @@ export class FileOnDiskContentProvider implements ITextModelContentProvider { // Make sure to keep contents on disk up to date when it changes if (!this.fileWatcher) { this.fileWatcher = this.fileService.onFileChanges(changes => { - if (changes.contains(fileOnDiskResource, FileChangeType.UPDATED)) { // + if (changes.contains(fileOnDiskResource, FileChangeType.UPDATED)) { this.resolveEditorModel(resource, false /* do not create if missing */).done(null, onUnexpectedError); // update model when resource changes } }); diff --git a/src/vs/workbench/parts/files/electron-browser/explorerViewlet.ts b/src/vs/workbench/parts/files/electron-browser/explorerViewlet.ts index 714e365c394..fe8a56d8938 100644 --- a/src/vs/workbench/parts/files/electron-browser/explorerViewlet.ts +++ b/src/vs/workbench/parts/files/electron-browser/explorerViewlet.ts @@ -10,7 +10,6 @@ import { localize } from 'vs/nls'; import { IActionRunner } from 'vs/base/common/actions'; import { TPromise } from 'vs/base/common/winjs.base'; import * as DOM from 'vs/base/browser/dom'; -import { Builder } from 'vs/base/browser/builder'; import { VIEWLET_ID, ExplorerViewletVisibleContext, IFilesConfiguration, OpenEditorsVisibleContext, OpenEditorsVisibleCondition, IExplorerViewlet } from 'vs/workbench/parts/files/common/files'; import { PersistentViewsViewlet, IViewletViewOptions, ViewsViewletPanel } from 'vs/workbench/browser/parts/views/viewsViewlet'; import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; @@ -171,11 +170,10 @@ export class ExplorerViewlet extends PersistentViewsViewlet implements IExplorer this._register(this.contextService.onDidChangeWorkspaceName(e => this.updateTitleArea())); } - async create(parent: Builder): TPromise { + async create(parent: HTMLElement): TPromise { await super.create(parent); - const el = parent.getHTMLElement(); - DOM.addClass(el, 'explorer-viewlet'); + DOM.addClass(parent, 'explorer-viewlet'); } private isOpenEditorsVisible(): boolean { diff --git a/src/vs/workbench/parts/files/electron-browser/fileActions.ts b/src/vs/workbench/parts/files/electron-browser/fileActions.ts index 0056fa9eb60..5e58cd18646 100644 --- a/src/vs/workbench/parts/files/electron-browser/fileActions.ts +++ b/src/vs/workbench/parts/files/electron-browser/fileActions.ts @@ -13,6 +13,7 @@ import { sequence, ITask, always } from 'vs/base/common/async'; import * as paths from 'vs/base/common/paths'; import * as resources from 'vs/base/common/resources'; import URI from 'vs/base/common/uri'; +import { posix } from 'path'; import * as errors from 'vs/base/common/errors'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import * as strings from 'vs/base/common/strings'; @@ -103,11 +104,12 @@ export class BaseErrorReportingAction extends Action { } protected onErrorWithRetry(error: any, retry: () => TPromise): void { - this._notificationService.prompt(Severity.Error, toErrorMessage(error, false), [nls.localize('retry', "Retry")]).then(choice => { - if (choice === 0) { - retry(); - } - }); + this._notificationService.prompt(Severity.Error, toErrorMessage(error, false), + [{ + label: nls.localize('retry', "Retry"), + run: () => retry() + }] + ); } } @@ -158,7 +160,15 @@ class TriggerRenameFileAction extends BaseFileAction { this._updateEnablement(); } - public validateFileName(parent: ExplorerItem, name: string): string { + public validateFileName(name: string): string { + const names: string[] = name.split(/[\\/]/).filter(part => !!part); + if (names.length > 1) { // error only occurs on multi-path + const comparer = isLinux ? strings.compare : strings.compareIgnoreCase; + if (comparer(names[0], this.element.name) === 0) { + return nls.localize('renameWhenSourcePathIsParentOfTargetError', "Please use the 'New Folder' or 'New File' command to add children to an existing folder"); + } + } + return this.renameAction.validateFileName(this.element.parent, name); } @@ -180,7 +190,7 @@ class TriggerRenameFileAction extends BaseFileAction { viewletState.setEditable(stat, { action: this.renameAction, validator: (value) => { - const message = this.validateFileName(this.element.parent, value); + const message = this.validateFileName(value); if (!message) { return null; @@ -381,6 +391,10 @@ export class BaseNewAction extends BaseFileAction { if (!folder) { return TPromise.wrapError(new Error('Invalid parent folder to create.')); } + if (!!folder.getChild(NewStatPlaceholder.NAME)) { + // Do not allow to creatae a new file/folder while in the process of creating a new file/folder #47606 + return TPromise.as(new Error('Parent folder is already in the process of creating a file')); + } return this.tree.reveal(folder, 0.5).then(() => { return this.tree.expand(folder).then(() => { @@ -631,9 +645,8 @@ class BaseDeleteFileAction extends BaseFileAction { // Confirm for moving to trash else if (this.useTrash) { - const message = distinctElements.length > 1 ? getConfirmMessage(nls.localize('confirmMoveTrashMessageMultiple', "Are you sure you want to delete the following {0} files?", distinctElements.length), distinctElements.map(e => e.resource)) - : distinctElements[0].isDirectory ? nls.localize('confirmMoveTrashMessageFolder', "Are you sure you want to delete '{0}' and its contents?", distinctElements[0].name) - : nls.localize('confirmMoveTrashMessageFile', "Are you sure you want to delete '{0}'?", distinctElements[0].name); + const message = this.getMoveToTrashMessage(distinctElements); + confirmDeletePromise = this.dialogService.confirm({ message, detail: isWindows ? nls.localize('undoBin', "You can restore from the Recycle Bin.") : nls.localize('undoTrash', "You can restore from the Trash."), @@ -647,9 +660,7 @@ class BaseDeleteFileAction extends BaseFileAction { // Confirm for deleting permanently else { - const message = distinctElements.length > 1 ? getConfirmMessage(nls.localize('confirmDeleteMessageMultiple', "Are you sure you want to permanently delete the following {0} files?", distinctElements.length), distinctElements.map(e => e.resource)) - : distinctElements[0].isDirectory ? nls.localize('confirmDeleteMessageFolder', "Are you sure you want to permanently delete '{0}' and its contents?", distinctElements[0].name) - : nls.localize('confirmDeleteMessageFile', "Are you sure you want to permanently delete '{0}'?", distinctElements[0].name); + const message = this.getDeleteMessage(distinctElements); confirmDeletePromise = this.dialogService.confirm({ message, detail: nls.localize('irreversible', "This action is irreversible!"), @@ -722,10 +733,57 @@ class BaseDeleteFileAction extends BaseFileAction { }); }); } + + private getMoveToTrashMessage(distinctElements: ExplorerItem[]): string { + if (this.containsBothDirectoryAndFile(distinctElements)) { + return getConfirmMessage(nls.localize('confirmMoveTrashMessageFilesAndDirectories', "Are you sure you want to delete the following {0} files/directories and its contents?", distinctElements.length), distinctElements.map(e => e.resource)); + } + + if (distinctElements.length > 1) { + if (distinctElements[0].isDirectory) { + return getConfirmMessage(nls.localize('confirmMoveTrashMessageMultipleDirectories', "Are you sure you want to delete the following {0} directories and its contents?", distinctElements.length), distinctElements.map(e => e.resource)); + } + + return getConfirmMessage(nls.localize('confirmMoveTrashMessageMultiple', "Are you sure you want to delete the following {0} files?", distinctElements.length), distinctElements.map(e => e.resource)); + } + + if (distinctElements[0].isDirectory) { + return nls.localize('confirmMoveTrashMessageFolder', "Are you sure you want to delete '{0}' and its contents?", distinctElements[0].name); + } + + return nls.localize('confirmMoveTrashMessageFile', "Are you sure you want to delete '{0}'?", distinctElements[0].name); + } + + private getDeleteMessage(distinctElements: ExplorerItem[]): string { + if (this.containsBothDirectoryAndFile(distinctElements)) { + return getConfirmMessage(nls.localize('confirmDeleteMessageFilesAndDirectories', "Are you sure you want to permanently delete the following {0} files/directories and its contents?", distinctElements.length), distinctElements.map(e => e.resource)); + } + + if (distinctElements.length > 1) { + if (distinctElements[0].isDirectory) { + return getConfirmMessage(nls.localize('confirmDeleteMessageMultipleDirectories', "Are you sure you want to permanently delete the following {0} directories and its contents?", distinctElements.length), distinctElements.map(e => e.resource)); + } + + return getConfirmMessage(nls.localize('confirmDeleteMessageMultiple', "Are you sure you want to permanently delete the following {0} files?", distinctElements.length), distinctElements.map(e => e.resource)); + } + + if (distinctElements[0].isDirectory) { + return nls.localize('confirmDeleteMessageFolder', "Are you sure you want to permanently delete '{0}' and its contents?", distinctElements[0].name); + } + + return nls.localize('confirmDeleteMessageFile', "Are you sure you want to permanently delete '{0}'?", distinctElements[0].name); + } + + private containsBothDirectoryAndFile(distinctElements: ExplorerItem[]): boolean { + const directories = distinctElements.filter(element => element.isDirectory); + const files = distinctElements.filter(element => !element.isDirectory); + + return directories.length > 0 && files.length > 0; + } } -/* Import File */ -export class ImportFileAction extends BaseFileAction { +/* Add File */ +export class AddFilesAction extends BaseFileAction { private tree: ITree; @@ -739,7 +797,7 @@ export class ImportFileAction extends BaseFileAction { @INotificationService notificationService: INotificationService, @ITextFileService textFileService: ITextFileService ) { - super('workbench.files.action.importFile', nls.localize('importFiles', "Import Files"), fileService, notificationService, textFileService); + super('workbench.files.action.addFile', nls.localize('addFiles', "Add Files"), fileService, notificationService, textFileService); this.tree = tree; this.element = element; @@ -752,10 +810,10 @@ export class ImportFileAction extends BaseFileAction { } public run(resources: URI[]): TPromise { - const importPromise = TPromise.as(null).then(() => { + const addPromise = TPromise.as(null).then(() => { if (resources && resources.length > 0) { - // Find parent for import + // Find parent to add to let targetElement: ExplorerItem; if (this.element) { targetElement = this.element; @@ -796,15 +854,15 @@ export class ImportFileAction extends BaseFileAction { return void 0; } - // Run import in sequence - const importPromisesFactory: ITask>[] = []; + // Run add in sequence + const addPromisesFactory: ITask>[] = []; resources.forEach(resource => { - importPromisesFactory.push(() => { + addPromisesFactory.push(() => { const sourceFile = resource; const targetFile = targetElement.resource.with({ path: paths.join(targetElement.resource.path, paths.basename(sourceFile.path)) }); // if the target exists and is dirty, make sure to revert it. otherwise the dirty contents - // of the target file would replace the contents of the imported file. since we already + // of the target file would replace the contents of the added file. since we already // confirmed the overwrite before, this is OK. let revertPromise = TPromise.wrap(null); if (this.textFileService.isDirty(targetFile)) { @@ -812,18 +870,19 @@ export class ImportFileAction extends BaseFileAction { } return revertPromise.then(() => { - return this.fileService.importFile(sourceFile, targetElement.resource).then(res => { + const target = targetElement.resource.with({ path: posix.join(targetElement.resource.path, posix.basename(sourceFile.path)) }); + return this.fileService.copyFile(sourceFile, target, true).then(stat => { - // if we only import one file, just open it directly + // if we only add one file, just open it directly if (resources.length === 1) { - this.editorService.openEditor({ resource: res.stat.resource, options: { pinned: true } }).done(null, errors.onUnexpectedError); + this.editorService.openEditor({ resource: stat.resource, options: { pinned: true } }).done(null, errors.onUnexpectedError); } }, error => this.onError(error)); }); }); }); - return sequence(importPromisesFactory); + return sequence(addPromisesFactory); }); }); } @@ -831,7 +890,7 @@ export class ImportFileAction extends BaseFileAction { return void 0; }); - return importPromise.then(() => { + return addPromise.then(() => { this.tree.clearHighlight(); }, (error: any) => { this.onError(error); @@ -1352,25 +1411,22 @@ export function validateFileName(parent: ExplorerItem, name: string, allowOverwr return nls.localize('emptyFileNameError', "A file or folder name must be provided."); } + // Relative paths only + if (name[0] === '/' || name[0] === '\\') { + return nls.localize('fileNameStartsWithSlashError', "A file or folder name cannot start with a slash."); + } + const names: string[] = name.split(/[\\/]/).filter(part => !!part); + const analyzedPath = analyzePath(parent, names); // Do not allow to overwrite existing file - if (!allowOverwriting) { - let p = parent; - const alreadyExisting = names.every((folderName) => { - let { exists, child } = alreadyExists(p, folderName); + if (!allowOverwriting && analyzedPath.fullPathAlreadyExists) { + return nls.localize('fileNameExistsError', "A file or folder **{0}** already exists at this location. Please choose a different name.", name); + } - if (!exists) { - return false; - } else { - p = child; - return true; - } - }); - - if (alreadyExisting) { - return nls.localize('fileNameExistsError', "A file or folder **{0}** already exists at this location. Please choose a different name.", name); - } + // A file must always be a leaf + if (analyzedPath.lastExistingPathSegment.isFile) { + return nls.localize('fileUsedAsFolderError', "**{0}** is a file and cannot have any descendants.", analyzedPath.lastExistingPathSegment.name); } // Invalid File name @@ -1389,6 +1445,24 @@ export function validateFileName(parent: ExplorerItem, name: string, allowOverwr return null; } +function analyzePath(parent: ExplorerItem, pathNames: string[]): { fullPathAlreadyExists: boolean; lastExistingPathSegment: { isFile: boolean; name: string; } } { + let lastExistingPathSegment = { isFile: false, name: '' }; + + for (const name of pathNames) { + const { exists, child } = alreadyExists(parent, name); + + if (exists) { + lastExistingPathSegment = { isFile: !child.isDirectory, name }; + parent = child; + } else { + return { fullPathAlreadyExists: false, lastExistingPathSegment }; + } + } + + return { fullPathAlreadyExists: true, lastExistingPathSegment }; +} + + function alreadyExists(parent: ExplorerItem, name: string): { exists: boolean, child: ExplorerItem | undefined } { let duplicateChild: ExplorerItem; @@ -1413,13 +1487,12 @@ export function getWellFormedFileName(filename: string): string { return filename; } - // Trim whitespaces - filename = strings.trim(strings.trim(filename, ' '), '\t'); + // Trim tabs + filename = strings.trim(filename, '\t'); - // Remove trailing dots + // Remove trailing dots, slashes, and spaces filename = strings.rtrim(filename, '.'); - - // Remove trailing slashes + filename = strings.rtrim(filename, ' '); filename = strings.rtrim(filename, '/'); filename = strings.rtrim(filename, '\\'); diff --git a/src/vs/workbench/parts/files/electron-browser/fileCommands.ts b/src/vs/workbench/parts/files/electron-browser/fileCommands.ts index f19b8205f14..06a87609635 100644 --- a/src/vs/workbench/parts/files/electron-browser/fileCommands.ts +++ b/src/vs/workbench/parts/files/electron-browser/fileCommands.ts @@ -322,7 +322,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ const name = paths.basename(uri.fsPath); const editorLabel = nls.localize('modifiedLabel', "{0} (on disk) ↔ {1}", name, name); - return editorService.openEditor({ leftResource: URI.from({ scheme: COMPARE_WITH_SAVED_SCHEMA, path: uri.fsPath }), rightResource: resource, label: editorLabel }).then(() => void 0); + return editorService.openEditor({ leftResource: URI.from({ scheme: COMPARE_WITH_SAVED_SCHEMA, path: uri.fsPath }), rightResource: uri, label: editorLabel }).then(() => void 0); } return TPromise.as(true); @@ -386,9 +386,11 @@ CommandsRegistry.registerCommand({ } }); -function revealResourcesInOS(resources: URI[], windowsService: IWindowsService, notificationService: INotificationService): void { +function revealResourcesInOS(resources: URI[], windowsService: IWindowsService, notificationService: INotificationService, workspaceContextService: IWorkspaceContextService): void { if (resources.length) { sequence(resources.map(r => () => windowsService.showItemInFolder(paths.normalize(r.fsPath, true)))); + } else if (workspaceContextService.getWorkspace().folders.length) { + windowsService.showItemInFolder(paths.normalize(workspaceContextService.getWorkspace().folders[0].uri.fsPath, true)); } else { notificationService.info(nls.localize('openFileToReveal', "Open a file first to reveal")); } @@ -396,14 +398,14 @@ function revealResourcesInOS(resources: URI[], windowsService: IWindowsService, KeybindingsRegistry.registerCommandAndKeybindingRule({ id: REVEAL_IN_OS_COMMAND_ID, weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), - when: ExplorerFocusCondition, + when: undefined, primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_R, win: { primary: KeyMod.Shift | KeyMod.Alt | KeyCode.KEY_R }, handler: (accessor: ServicesAccessor, resource: URI | object) => { const resources = getMultiSelectedResources(resource, accessor.get(IListService), accessor.get(IWorkbenchEditorService)); - revealResourcesInOS(resources, accessor.get(IWindowsService), accessor.get(INotificationService)); + revealResourcesInOS(resources, accessor.get(IWindowsService), accessor.get(INotificationService), accessor.get(IWorkspaceContextService)); } }); KeybindingsRegistry.registerCommandAndKeybindingRule({ @@ -415,7 +417,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ const editorService = accessor.get(IWorkbenchEditorService); const activeInput = editorService.getActiveEditorInput(); const resources = activeInput && activeInput.getResource() ? [activeInput.getResource()] : []; - revealResourcesInOS(resources, accessor.get(IWindowsService), accessor.get(INotificationService)); + revealResourcesInOS(resources, accessor.get(IWindowsService), accessor.get(INotificationService), accessor.get(IWorkspaceContextService)); } }); @@ -430,7 +432,7 @@ function resourcesToClipboard(resources: URI[], clipboardService: IClipboardServ } KeybindingsRegistry.registerCommandAndKeybindingRule({ weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), - when: ExplorerFocusCondition, + when: undefined, primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_C, win: { primary: KeyMod.Shift | KeyMod.Alt | KeyCode.KEY_C diff --git a/src/vs/workbench/parts/files/electron-browser/files.contribution.ts b/src/vs/workbench/parts/files/electron-browser/files.contribution.ts index 69e208012e1..1da4ee90a94 100644 --- a/src/vs/workbench/parts/files/electron-browser/files.contribution.ts +++ b/src/vs/workbench/parts/files/electron-browser/files.contribution.ts @@ -15,7 +15,7 @@ import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/wor import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; import { IEditorInputFactory, EditorInput, IFileEditorInput, IEditorInputFactoryRegistry, Extensions as EditorInputExtensions } from 'vs/workbench/common/editor'; import { AutoSaveConfiguration, HotExitConfiguration, SUPPORTED_ENCODINGS } from 'vs/platform/files/common/files'; -import { FILE_EDITOR_INPUT_ID, VIEWLET_ID, SortOrderConfiguration } from 'vs/workbench/parts/files/common/files'; +import { VIEWLET_ID, SortOrderConfiguration, FILE_EDITOR_INPUT_ID } from 'vs/workbench/parts/files/common/files'; import { FileEditorTracker } from 'vs/workbench/parts/files/browser/editors/fileEditorTracker'; import { SaveErrorHandler } from 'vs/workbench/parts/files/electron-browser/saveErrorHandler'; import { FileEditorInput } from 'vs/workbench/parts/files/common/editors/fileEditorInput'; @@ -286,7 +286,7 @@ configurationRegistry.registerConfiguration({ 'files.maxMemoryForLargeFilesMB': { 'type': 'number', 'default': 4096, - 'description': nls.localize('maxMemoryForLargeFilesMB', "The new limit on memory in MB to be used by the application when relaunching to open large files. If you wish to start with a higher limit, you can launch the application from command line with --max-memory=NEWSIZE.") + 'description': nls.localize('maxMemoryForLargeFilesMB', "Controls the memory available to VS Code after restart when trying to open large files. Same affect as specifying --max-memory=NEWSIZE on the command line.") } } }); diff --git a/src/vs/workbench/parts/files/electron-browser/media/explorerviewlet.css b/src/vs/workbench/parts/files/electron-browser/media/explorerviewlet.css index 9cd2408fa2c..eb657380a67 100644 --- a/src/vs/workbench/parts/files/electron-browser/media/explorerviewlet.css +++ b/src/vs/workbench/parts/files/electron-browser/media/explorerviewlet.css @@ -109,7 +109,6 @@ .monaco-workbench.linux .explorer-viewlet .explorer-item .monaco-inputbox, .monaco-workbench.mac .explorer-viewlet .explorer-item .monaco-inputbox { height: 22px; - margin-left: -1px; } .explorer-viewlet .explorer-item .monaco-inputbox > .wrapper > .input { diff --git a/src/vs/workbench/parts/files/electron-browser/saveErrorHandler.ts b/src/vs/workbench/parts/files/electron-browser/saveErrorHandler.ts index 56bf6d7a656..8ddc5c94b45 100644 --- a/src/vs/workbench/parts/files/electron-browser/saveErrorHandler.ts +++ b/src/vs/workbench/parts/files/electron-browser/saveErrorHandler.ts @@ -102,7 +102,7 @@ export class SaveErrorHandler implements ISaveErrorHandler, IWorkbenchContributi private onFileSavedOrReverted(resource: URI): void { const messageHandle = this.messages.get(resource); if (messageHandle) { - messageHandle.dispose(); + messageHandle.close(); this.messages.delete(resource); } } @@ -179,7 +179,7 @@ export class SaveErrorHandler implements ISaveErrorHandler, IWorkbenchContributi // Show message and keep function to hide in case the file gets saved/reverted const handle = this.notificationService.notify({ severity: Severity.Error, message, actions }); - once(handle.onDidDispose)(() => dispose(...actions.primary, ...actions.secondary)); + once(handle.onDidClose)(() => dispose(...actions.primary, ...actions.secondary)); this.messages.set(model.getResource(), handle); } @@ -193,7 +193,7 @@ export class SaveErrorHandler implements ISaveErrorHandler, IWorkbenchContributi const pendingResolveSaveConflictMessages: INotificationHandle[] = []; function clearPendingResolveSaveConflictMessages(): void { while (pendingResolveSaveConflictMessages.length > 0) { - pendingResolveSaveConflictMessages.pop().dispose(); + pendingResolveSaveConflictMessages.pop().close(); } } @@ -265,7 +265,7 @@ class ResolveSaveConflictAction extends Action { actions.secondary.push(this.instantiationService.createInstance(DoNotShowResolveConflictLearnMoreAction)); const handle = this.notificationService.notify({ severity: Severity.Info, message: conflictEditorHelp, actions }); - once(handle.onDidDispose)(() => dispose(...actions.primary, ...actions.secondary)); + once(handle.onDidClose)(() => dispose(...actions.primary, ...actions.secondary)); pendingResolveSaveConflictMessages.push(handle); }); } diff --git a/src/vs/workbench/parts/files/electron-browser/views/emptyView.ts b/src/vs/workbench/parts/files/electron-browser/views/emptyView.ts index e2253c98749..f0430ac6ed2 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/emptyView.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/emptyView.ts @@ -56,7 +56,7 @@ export class EmptyView extends ViewsViewletPanel { let section = $('div.section').appendTo(container); - this.button = new Button(section); + this.button = new Button(section.getHTMLElement()); attachButtonStyler(this.button, this.themeService); this.disposables.push(this.button.onDidClick(() => { diff --git a/src/vs/workbench/parts/files/electron-browser/views/explorerDecorationsProvider.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerDecorationsProvider.ts index c5ed9e10afd..dcbb387a155 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerDecorationsProvider.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerDecorationsProvider.ts @@ -22,7 +22,7 @@ export class ExplorerDecorationsProvider implements IDecorationsProvider { @IWorkspaceContextService contextService: IWorkspaceContextService ) { contextService.onDidChangeWorkspaceFolders(e => { - this._onDidChange.fire(e.changed.map(wf => wf.uri)); + this._onDidChange.fire(e.changed.concat(e.added).map(wf => wf.uri)); }); } diff --git a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts index 5ee30960d13..ea60112f840 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts @@ -6,7 +6,6 @@ import * as nls from 'vs/nls'; import { TPromise } from 'vs/base/common/winjs.base'; -import { Builder, $ } from 'vs/base/browser/builder'; import URI from 'vs/base/common/uri'; import { ThrottledDelayer, Delayer } from 'vs/base/common/async'; import * as errors from 'vs/base/common/errors'; @@ -160,7 +159,7 @@ export class ExplorerView extends TreeViewsViewletPanel implements IExplorerView public renderBody(container: HTMLElement): void { this.treeContainer = DOM.append(container, DOM.$('.explorer-folders-view')); - this.tree = this.createViewer($(this.treeContainer)); + this.tree = this.createViewer(this.treeContainer); if (this.toolbar) { this.toolbar.setActions(this.getActions(), this.getSecondaryActions())(); @@ -227,7 +226,7 @@ export class ExplorerView extends TreeViewsViewletPanel implements IExplorerView this.settings[ExplorerView.MEMENTO_LAST_ACTIVE_FILE_RESOURCE] = activeFile.toString(); // Select file if input is inside workspace - if (this.isVisible() && this.contextService.isInsideWorkspace(activeFile)) { + if (this.isVisible() && !this.isDisposed && this.contextService.isInsideWorkspace(activeFile)) { const selection = this.hasSingleSelection(activeFile); if (!selection) { this.select(activeFile).done(null, errors.onUnexpectedError); @@ -394,7 +393,7 @@ export class ExplorerView extends TreeViewsViewletPanel implements IExplorerView return model; } - private createViewer(container: Builder): WorkbenchTree { + private createViewer(container: HTMLElement): WorkbenchTree { const dataSource = this.instantiationService.createInstance(FileDataSource); const renderer = this.instantiationService.createInstance(FileRenderer, this.viewletState); const controller = this.instantiationService.createInstance(FileController); @@ -406,7 +405,7 @@ export class ExplorerView extends TreeViewsViewletPanel implements IExplorerView const dnd = this.instantiationService.createInstance(FileDragAndDrop); const accessibilityProvider = this.instantiationService.createInstance(FileAccessibilityProvider); - this.explorerViewer = this.instantiationService.createInstance(FileIconThemableWorkbenchTree, container.getHTMLElement(), { + this.explorerViewer = this.instantiationService.createInstance(FileIconThemableWorkbenchTree, container, { dataSource, renderer, controller, @@ -471,7 +470,7 @@ export class ExplorerView extends TreeViewsViewletPanel implements IExplorerView } // Add - if (e.operation === FileOperation.CREATE || e.operation === FileOperation.IMPORT || e.operation === FileOperation.COPY) { + if (e.operation === FileOperation.CREATE || e.operation === FileOperation.COPY) { const addedElement = e.target; const parentResource = resources.dirname(addedElement.resource); const parents = this.model.findAll(parentResource); @@ -611,24 +610,21 @@ export class ExplorerView extends TreeViewsViewletPanel implements IExplorerView } private shouldRefreshFromEvent(e: FileChangesEvent): boolean { - - // Filter to the ones we care - e = this.filterFileEvents(e); - if (!this.isCreated) { return false; } - if (e.gotAdded()) { - const added = e.getAdded(); + // Filter to the ones we care + e = this.filterToViewRelevantEvents(e); + + // Handle added files/folders + const added = e.getAdded(); + if (added.length) { // Check added: Refresh if added file/folder is not part of resolved root and parent is part of it const ignoredPaths: { [resource: string]: boolean } = <{ [resource: string]: boolean }>{}; for (let i = 0; i < added.length; i++) { const change = added[i]; - if (!this.contextService.isInsideWorkspace(change.resource)) { - continue; // out of workspace file - } // Find parent const parent = resources.dirname(change.resource); @@ -651,15 +647,13 @@ export class ExplorerView extends TreeViewsViewletPanel implements IExplorerView } } - if (e.gotDeleted()) { - const deleted = e.getDeleted(); + // Handle deleted files/folders + const deleted = e.getDeleted(); + if (deleted.length) { // Check deleted: Refresh if deleted file/folder part of resolved root for (let j = 0; j < deleted.length; j++) { const del = deleted[j]; - if (!this.contextService.isInsideWorkspace(del.resource)) { - continue; // out of workspace file - } if (this.model.findClosest(del.resource)) { return true; @@ -667,15 +661,13 @@ export class ExplorerView extends TreeViewsViewletPanel implements IExplorerView } } - if (this.sortOrder === SortOrderConfiguration.MODIFIED && e.gotUpdated()) { + // Handle updated files/folders if we sort by modified + if (this.sortOrder === SortOrderConfiguration.MODIFIED) { const updated = e.getUpdated(); // Check updated: Refresh if updated file/folder part of resolved root for (let j = 0; j < updated.length; j++) { const upd = updated[j]; - if (!this.contextService.isInsideWorkspace(upd.resource)) { - continue; // out of workspace file - } if (this.model.findClosest(upd.resource)) { return true; @@ -686,8 +678,12 @@ export class ExplorerView extends TreeViewsViewletPanel implements IExplorerView return false; } - private filterFileEvents(e: FileChangesEvent): FileChangesEvent { + private filterToViewRelevantEvents(e: FileChangesEvent): FileChangesEvent { return new FileChangesEvent(e.changes.filter(change => { + if (change.type === FileChangeType.UPDATED && this.sortOrder !== SortOrderConfiguration.MODIFIED) { + return false; // we only are about updated if we sort by modified time + } + if (!this.contextService.isInsideWorkspace(change.resource)) { return false; // exclude changes for resources outside of workspace } @@ -701,7 +697,7 @@ export class ExplorerView extends TreeViewsViewletPanel implements IExplorerView } private refreshFromEvent(newRoots: IWorkspaceFolder[] = []): void { - if (this.isVisible()) { + if (this.isVisible() && !this.isDisposed) { this.explorerRefreshDelayer.trigger(() => { if (!this.explorerViewer.getHighlight()) { return this.doRefresh(newRoots.map(r => r.uri)).then(() => { diff --git a/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts index 9b412ed7155..1a418a43fc5 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts @@ -8,6 +8,7 @@ import { TPromise } from 'vs/base/common/winjs.base'; import * as nls from 'vs/nls'; import * as objects from 'vs/base/common/objects'; import * as DOM from 'vs/base/browser/dom'; +import * as path from 'path'; import URI from 'vs/base/common/uri'; import { once } from 'vs/base/common/functional'; import * as paths from 'vs/base/common/paths'; @@ -23,8 +24,7 @@ import { IDisposable, dispose, empty as EmptyDisposable } from 'vs/base/common/l import { IFilesConfiguration, SortOrder } from 'vs/workbench/parts/files/common/files'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { FileOperationError, FileOperationResult, IFileService, FileKind } from 'vs/platform/files/common/files'; -import { ResourceMap } from 'vs/base/common/map'; -import { DuplicateFileAction, ImportFileAction, IEditableData, IFileViewletState, FileCopiedContext } from 'vs/workbench/parts/files/electron-browser/fileActions'; +import { DuplicateFileAction, AddFilesAction, IEditableData, IFileViewletState, FileCopiedContext } from 'vs/workbench/parts/files/electron-browser/fileActions'; import { IDataSource, ITree, IAccessibilityProvider, IRenderer, ContextMenuEvent, ISorter, IFilter, IDragAndDropData, IDragOverReaction, DRAG_OVER_ACCEPT_BUBBLE_DOWN, DRAG_OVER_ACCEPT_BUBBLE_DOWN_COPY, DRAG_OVER_ACCEPT_BUBBLE_UP, DRAG_OVER_ACCEPT_BUBBLE_UP_COPY, DRAG_OVER_REJECT } from 'vs/base/parts/tree/browser/tree'; import { DesktopDragAndDropData, ExternalElementsDragAndDropData } from 'vs/base/parts/tree/browser/treeDnd'; import { ClickBehavior } from 'vs/base/parts/tree/browser/treeDefaults'; @@ -49,7 +49,6 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IWindowService } from 'vs/platform/windows/common/windows'; import { IWorkspaceEditingService } from 'vs/workbench/services/workspace/common/workspaceEditing'; import { extractResources, SimpleFileResourceDragAndDrop, CodeDataTransfers, fillResourceDataTransfers } from 'vs/workbench/browser/dnd'; -import { relative } from 'path'; import { WorkbenchTree, WorkbenchTreeController } from 'vs/platform/list/browser/listService'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { DataTransfers } from 'vs/base/browser/dnd'; @@ -76,7 +75,7 @@ export class FileDataSource implements IDataSource { } public hasChildren(tree: ITree, stat: ExplorerItem | Model): boolean { - return stat instanceof Model || (stat instanceof ExplorerItem && stat.isDirectory); + return stat instanceof Model || (stat instanceof ExplorerItem && (stat.isDirectory || stat.isRoot)); } public getChildren(tree: ITree, stat: ExplorerItem | Model): TPromise { @@ -99,9 +98,12 @@ export class FileDataSource implements IDataSource { const modelDirStat = ExplorerItem.create(dirStat, stat.root); // Add children to folder - modelDirStat.getChildrenArray().forEach(child => { - stat.addChild(child); - }); + const children = modelDirStat.getChildrenArray(); + if (children) { + children.forEach(child => { + stat.addChild(child); + }); + } stat.isDirectoryResolved = true; @@ -144,24 +146,24 @@ export class FileDataSource implements IDataSource { } export class FileViewletState implements IFileViewletState { - private editableStats: ResourceMap; + private editableStats: Map; constructor() { - this.editableStats = new ResourceMap(); + this.editableStats = new Map(); } public getEditableData(stat: ExplorerItem): IEditableData { - return this.editableStats.get(stat.resource); + return this.editableStats.get(stat); } public setEditable(stat: ExplorerItem, editableData: IEditableData): void { if (editableData) { - this.editableStats.set(stat.resource, editableData); + this.editableStats.set(stat, editableData); } } public clearEditable(stat: ExplorerItem): void { - this.editableStats.delete(stat.resource); + this.editableStats.delete(stat); } } @@ -226,6 +228,7 @@ export class FileRenderer implements IRenderer { } public disposeTemplate(tree: ITree, templateId: string, templateData: IFileTemplateData): void { + templateData.elementDisposable.dispose(); templateData.label.dispose(); } @@ -323,12 +326,12 @@ export class FileRenderer implements IRenderer { } }), DOM.addStandardDisposableListener(inputBox.inputElement, DOM.EventType.KEY_UP, (e: IKeyboardEvent) => { - const initialRelPath: string = relative(stat.root.resource.fsPath, stat.parent.resource.fsPath); + const initialRelPath: string = path.relative(stat.root.resource.path, stat.parent.resource.path); let projectFolderName: string = ''; if (this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE) { - projectFolderName = paths.basename(stat.root.resource.fsPath); // show root folder name in multi-folder project + projectFolderName = paths.basename(stat.root.resource.path); // show root folder name in multi-folder project } - this.displayCurrentPath(inputBox, initialRelPath, fileKind, projectFolderName); + this.displayCurrentPath(inputBox, initialRelPath, projectFolderName, editableData.action.id); }), DOM.addDisposableListener(inputBox.inputElement, DOM.EventType.BLUR, () => { done(inputBox.isInputValid(), true); @@ -338,20 +341,39 @@ export class FileRenderer implements IRenderer { ]; } - private displayCurrentPath(inputBox: InputBox, initialRelPath: string, fileKind: FileKind, projectFolderName: string = '') { + private displayCurrentPath(inputBox: InputBox, initialRelPath: string, projectFolderName: string = '', actionID: string) { if (inputBox.validate()) { const value = inputBox.value; - if (value && value.search(/[\\/]/) !== -1) { // only show if there's a slash - let newPath = paths.normalize(paths.join(initialRelPath, value), true); - newPath = rtrim(newPath, paths.nativeSep); - const fileType: string = FileKind[fileKind].toLowerCase(); + if (value && /.[\\/]./.test(value)) { // only show if there's at least one slash enclosed in the string + let displayPath = path.normalize(path.join(projectFolderName, initialRelPath, value)); + displayPath = rtrim(displayPath, paths.nativeSep); + + const indexLastSlash: number = displayPath.lastIndexOf(paths.nativeSep); + const name: string = displayPath.substring(indexLastSlash + 1); + const leadingPathPart: string = displayPath.substring(0, indexLastSlash); + + let msg: string; + switch (actionID) { + case 'workbench.files.action.createFileFromExplorer': + msg = nls.localize('createFileFromExplorerInfoMessage', "Create file **{0}** in **{1}**", name, leadingPathPart); + break; + case 'workbench.files.action.renameFile': + msg = nls.localize('renameFileFromExplorerInfoMessage', "Move and rename to **{0}**", displayPath); + break; + case 'workbench.files.action.createFolderFromExplorer': // fallthrough + default: + msg = nls.localize('createFolderFromExplorerInfoMessage', "Create folder **{0}** in **{1}**", name, leadingPathPart); + } inputBox.showMessage({ type: MessageType.INFO, - content: nls.localize('constructedPath', "Create {0} in **{1}**", fileType, newPath), + content: msg, formatContent: true }); } + else { // fixes #46744: inputbox hides again if all slashes are removed + inputBox.hideMessage(); + } } } } @@ -693,15 +715,17 @@ export class FileFilter implements IFilter { } // Workaround for O(N^2) complexity (https://github.com/Microsoft/vscode/issues/9962) - let siblingNames = stat.parent && stat.parent.getChildrenNames(); - if (siblingNames && siblingNames.length > FileFilter.MAX_SIBLINGS_FILTER_THRESHOLD) { - siblingNames = void 0; + let siblingsFn: () => string[]; + let siblingCount = stat.parent && stat.parent.getChildrenCount(); + if (siblingCount && siblingCount > FileFilter.MAX_SIBLINGS_FILTER_THRESHOLD) { + siblingsFn = () => void 0; + } else { + siblingsFn = () => stat.parent ? stat.parent.getChildrenNames() : void 0; } // Hide those that match Hidden Patterns - const siblingsFn = () => siblingNames; const expression = this.hiddenExpressionPerRoot.get(stat.root.resource.toString()) || Object.create(null); - if (glob.match(expression, paths.normalize(relative(stat.root.resource.fsPath, stat.resource.fsPath), true), siblingsFn)) { + if (glob.match(expression, paths.normalize(path.relative(stat.root.resource.path, stat.resource.path), true), siblingsFn)) { return false; // hidden through pattern } @@ -918,9 +942,9 @@ export class FileDragAndDrop extends SimpleFileResourceDragAndDrop { // Handle dropped files (only support FileStat as target) else if (target instanceof ExplorerItem) { - const importAction = this.instantiationService.createInstance(ImportFileAction, tree, target, null); + const addFilesAction = this.instantiationService.createInstance(AddFilesAction, tree, target, null); - return importAction.run(droppedResources.map(res => res.resource)); + return addFilesAction.run(droppedResources.map(res => res.resource)); } return void 0; diff --git a/src/vs/workbench/parts/files/test/browser/fileEditorInput.test.ts b/src/vs/workbench/parts/files/test/browser/fileEditorInput.test.ts index cf5fde01cd2..0d34ce79868 100644 --- a/src/vs/workbench/parts/files/test/browser/fileEditorInput.test.ts +++ b/src/vs/workbench/parts/files/test/browser/fileEditorInput.test.ts @@ -174,6 +174,18 @@ suite('Files - FileEditorInput', () => { }); }); + test('resolve handles too large files', function () { + const input = instantiationService.createInstance(FileEditorInput, toResource(this, '/foo/bar/updatefile.js'), void 0); + + accessor.textFileService.setResolveTextContentErrorOnce(new FileOperationError('error', FileOperationResult.FILE_TOO_LARGE)); + + return input.resolve(true).then(resolved => { + assert.ok(resolved); + + resolved.dispose(); + }); + }); + test('disposes model when not open anymore', function () { const resource = toResource(this, '/path/index.txt'); diff --git a/src/vs/workbench/parts/files/test/electron-browser/explorerModel.test.ts b/src/vs/workbench/parts/files/test/electron-browser/explorerModel.test.ts index 19cf2bf26c1..10791cfd06d 100644 --- a/src/vs/workbench/parts/files/test/electron-browser/explorerModel.test.ts +++ b/src/vs/workbench/parts/files/test/electron-browser/explorerModel.test.ts @@ -194,11 +194,6 @@ suite('Files - View Model', () => { assert(validateFileName(s, '') !== null); assert(validateFileName(s, ' ') !== null); assert(validateFileName(s, 'Read Me') === null, 'name containing space'); - assert(validateFileName(s, 'foo/bar') === null); - assert(validateFileName(s, 'foo\\bar') === null); - assert(validateFileName(s, 'all/slashes/are/same') === null); - assert(validateFileName(s, 'theres/one/different\\slash') === null); - assert(validateFileName(s, '/slashAtBeginning') === null); if (isWindows) { assert(validateFileName(s, 'foo:bar') !== null); @@ -236,6 +231,38 @@ suite('Files - View Model', () => { assert(validateFileName(s, 'foo') === null); }); + test('Validate Multi-Path File Names', function () { + const d = new Date().getTime(); + const wsFolder = createStat('/', 'workspaceFolder', true, false, 8096, d); + + assert(validateFileName(wsFolder, 'foo/bar') === null); + assert(validateFileName(wsFolder, 'foo\\bar') === null); + assert(validateFileName(wsFolder, 'all/slashes/are/same') === null); + assert(validateFileName(wsFolder, 'theres/one/different\\slash') === null); + assert(validateFileName(wsFolder, '/slashAtBeginning') !== null); + + // validation should detect if user tries to add a child to a file + const fileInRoot = createStat('/fileInRoot', 'fileInRoot', false, false, 8096, d); + wsFolder.addChild(fileInRoot); + assert(validateFileName(wsFolder, 'fileInRoot/aChild') !== null); + wsFolder.removeChild(fileInRoot); + + // attempting to add a child to a deeply nested file + const s1 = createStat('/path', 'path', true, false, 8096, d); + const s2 = createStat('/path/to', 'to', true, false, 8096, d); + const s3 = createStat('/path/to/stat', 'stat', true, false, 8096, d); + wsFolder.addChild(s1); + s1.addChild(s2); + s2.addChild(s3); + const fileDeeplyNested = createStat('/path/to/stat/fileNested', 'fileNested', false, false, 8096, d); + s3.addChild(fileDeeplyNested); + assert(validateFileName(wsFolder, '/path/to/stat/fileNested/aChild') !== null); + + // detect if path already exists + assert(validateFileName(wsFolder, '/path/to/stat/fileNested') !== null); + assert(validateFileName(wsFolder, '/path/to/stat/') !== null); + }); + test('Merge Local with Disk', function () { const d = new Date().toUTCString(); diff --git a/src/vs/workbench/parts/html/electron-browser/html.contribution.ts b/src/vs/workbench/parts/html/electron-browser/html.contribution.ts index 1d7f0a29cac..d94d09a13a7 100644 --- a/src/vs/workbench/parts/html/electron-browser/html.contribution.ts +++ b/src/vs/workbench/parts/html/electron-browser/html.contribution.ts @@ -18,8 +18,6 @@ import { IEditorGroupService } from 'vs/workbench/services/group/common/groupSer import { IExtensionsWorkbenchService } from 'vs/workbench/parts/extensions/common/extensions'; import { IEditorRegistry, EditorDescriptor, Extensions as EditorExtensions } from 'vs/workbench/browser/editor'; -import './webview.contribution'; - function getActivePreviewsForResource(accessor: ServicesAccessor, resource: URI | string) { const uri = resource instanceof URI ? resource : URI.parse(resource); return accessor.get(IWorkbenchEditorService).getVisibleEditors() diff --git a/src/vs/workbench/parts/html/electron-browser/htmlPreviewPart.ts b/src/vs/workbench/parts/html/electron-browser/htmlPreviewPart.ts index 7829900176f..344fef08a76 100644 --- a/src/vs/workbench/parts/html/electron-browser/htmlPreviewPart.ts +++ b/src/vs/workbench/parts/html/electron-browser/htmlPreviewPart.ts @@ -8,9 +8,8 @@ import { localize } from 'vs/nls'; import { TPromise } from 'vs/base/common/winjs.base'; import { ITextModel } from 'vs/editor/common/model'; -import { Dimension, Builder } from 'vs/base/browser/builder'; import { empty as EmptyDisposable, IDisposable, dispose, IReference } from 'vs/base/common/lifecycle'; -import { EditorOptions, EditorInput } from 'vs/workbench/common/editor'; +import { EditorOptions, EditorInput, EditorViewStateMemento } from 'vs/workbench/common/editor'; import { Position } from 'vs/platform/editor/common/editor'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { BaseTextEditorModel } from 'vs/workbench/common/editor/textEditorModel'; @@ -21,13 +20,12 @@ import { ITextModelService, ITextEditorModel } from 'vs/editor/common/services/r import { Parts, IPartService } from 'vs/workbench/services/part/common/partService'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; - -import { Webview, WebviewOptions } from './webview'; import { IStorageService } from 'vs/platform/storage/common/storage'; -import { BaseWebviewEditor } from './baseWebviewEditor'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import URI from 'vs/base/common/uri'; import { Scope } from 'vs/workbench/common/memento'; +import { Dimension } from 'vs/base/browser/dom'; +import { BaseWebviewEditor } from 'vs/workbench/parts/webview/electron-browser/baseWebviewEditor'; +import { WebviewElement, WebviewOptions } from 'vs/workbench/parts/webview/electron-browser/webviewElement'; export interface HtmlPreviewEditorViewState { scrollYPercentage: number; @@ -51,6 +49,8 @@ export class HtmlPreviewPart extends BaseWebviewEditor { private _content: HTMLElement; private _scrollYPercentage: number = 0; + private editorViewStateMemento: EditorViewStateMemento; + constructor( @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @@ -59,10 +59,12 @@ export class HtmlPreviewPart extends BaseWebviewEditor { @IEnvironmentService private readonly _environmentService: IEnvironmentService, @IOpenerService private readonly _openerService: IOpenerService, @IPartService private readonly _partService: IPartService, - @IStorageService private readonly _storageService: IStorageService, + @IStorageService readonly _storageService: IStorageService, @ITextModelService private readonly _textModelResolverService: ITextModelService ) { super(HtmlPreviewPart.ID, telemetryService, themeService, contextKeyService); + + this.editorViewStateMemento = new EditorViewStateMemento(this.getMemento(_storageService, Scope.WORKSPACE), this.viewStateStorageKey); } dispose(): void { @@ -78,21 +80,21 @@ export class HtmlPreviewPart extends BaseWebviewEditor { super.dispose(); } - protected createEditor(parent: Builder): void { + protected createEditor(parent: HTMLElement): void { this._content = document.createElement('div'); this._content.style.position = 'absolute'; this._content.classList.add(HtmlPreviewPart.class); - parent.getHTMLElement().appendChild(this._content); + parent.appendChild(this._content); } - private get webview(): Webview { + private get webview(): WebviewElement { if (!this._webview) { let webviewOptions: WebviewOptions = {}; if (this.input && this.input instanceof HtmlInput) { webviewOptions = this.input.options; } - this._webview = new Webview( + this._webview = new WebviewElement( this._partService.getContainer(Parts.EDITOR_PART), this.themeService, this._environmentService, @@ -106,7 +108,7 @@ export class HtmlPreviewPart extends BaseWebviewEditor { this._webview.mountTo(this._content); if (this.input && this.input instanceof HtmlInput) { - const state = this.loadViewState(this.input.getResource()); + const state = this.loadHTMLPreviewViewState(this.input); this._scrollYPercentage = state ? state.scrollYPercentage : 0; this.webview.initialScrollProgress = this._scrollYPercentage; @@ -168,7 +170,7 @@ export class HtmlPreviewPart extends BaseWebviewEditor { public clearInput(): void { if (this.input instanceof HtmlInput) { - this.saveViewState(this.input.getResource(), { + this.saveHTMLPreviewViewState(this.input, { scrollYPercentage: this._scrollYPercentage }); } @@ -179,7 +181,7 @@ export class HtmlPreviewPart extends BaseWebviewEditor { public shutdown(): void { if (this.input instanceof HtmlInput) { - this.saveViewState(this.input.getResource(), { + this.saveHTMLPreviewViewState(this.input, { scrollYPercentage: this._scrollYPercentage }); } @@ -200,7 +202,7 @@ export class HtmlPreviewPart extends BaseWebviewEditor { if (this.input instanceof HtmlInput) { oldOptions = this.input.options; - this.saveViewState(this.input.getResource(), { + this.saveHTMLPreviewViewState(this.input, { scrollYPercentage: this._scrollYPercentage }); } @@ -237,7 +239,7 @@ export class HtmlPreviewPart extends BaseWebviewEditor { this.webview.contents = this.model.getLinesContent().join('\n'); } }); - const state = this.loadViewState(resourceUri); + const state = this.loadHTMLPreviewViewState(input); this._scrollYPercentage = state ? state.scrollYPercentage : 0; this.webview.baseUrl = resourceUri.toString(true); this.webview.options = input.options; @@ -253,34 +255,19 @@ export class HtmlPreviewPart extends BaseWebviewEditor { return this.getId() + '.editorViewState'; } - protected saveViewState(resource: URI | string, editorViewState: HtmlPreviewEditorViewState): void { - const memento = this.getMemento(this._storageService, Scope.WORKSPACE); - let editorViewStateMemento: { [key: string]: { [position: number]: HtmlPreviewEditorViewState } } = memento[this.viewStateStorageKey]; - if (!editorViewStateMemento) { - editorViewStateMemento = Object.create(null); - memento[this.viewStateStorageKey] = editorViewStateMemento; - } - - let fileViewState = editorViewStateMemento[resource.toString()]; - if (!fileViewState) { - fileViewState = Object.create(null); - editorViewStateMemento[resource.toString()] = fileViewState; - } - - if (typeof this.position === 'number') { - fileViewState[this.position] = editorViewState; - } + private saveHTMLPreviewViewState(input: HtmlInput, editorViewState: HtmlPreviewEditorViewState): void { + this.editorViewStateMemento.saveState(input, this.position, editorViewState); } - protected loadViewState(resource: URI | string): HtmlPreviewEditorViewState | null { - const memento = this.getMemento(this._storageService, Scope.WORKSPACE); - const editorViewStateMemento: { [key: string]: { [position: number]: HtmlPreviewEditorViewState } } = memento[this.viewStateStorageKey]; - if (editorViewStateMemento) { - const fileViewState = editorViewStateMemento[resource.toString()]; - if (fileViewState) { - return fileViewState[this.position]; - } - } - return null; + private loadHTMLPreviewViewState(input: HtmlInput): HtmlPreviewEditorViewState { + return this.editorViewStateMemento.loadState(input, this.position); + } + + protected saveMemento(): void { + + // ensure to first save our view state memento + this.editorViewStateMemento.save(); + + super.saveMemento(); } } diff --git a/src/vs/workbench/parts/html/electron-browser/webview.contribution.ts b/src/vs/workbench/parts/html/electron-browser/webview.contribution.ts deleted file mode 100644 index f6697f659f6..00000000000 --- a/src/vs/workbench/parts/html/electron-browser/webview.contribution.ts +++ /dev/null @@ -1,70 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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 { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; -import { ContextKeyExpr, } from 'vs/platform/contextkey/common/contextkey'; -import { KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; - -import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions'; -import { KEYBINDING_CONTEXT_WEBVIEWEDITOR_FIND_WIDGET_INPUT_FOCUSED, KEYBINDING_CONTEXT_WEBVIEWEDITOR_FOCUS, KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE } from './baseWebviewEditor'; -import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { ShowWebViewEditorFindTermCommand, HideWebViewEditorFindCommand, OpenWebviewDeveloperToolsAction, ReloadWebviewAction, ShowWebViewEditorFindWidgetCommand } from './webviewCommands'; - -const webviewDeveloperCategory = nls.localize('developer', "Developer"); - -const actionRegistry = Registry.as(ActionExtensions.WorkbenchActions); - -const showNextFindWdigetCommand = new ShowWebViewEditorFindWidgetCommand({ - id: ShowWebViewEditorFindWidgetCommand.ID, - precondition: KEYBINDING_CONTEXT_WEBVIEWEDITOR_FOCUS, - kbOpts: { - primary: KeyMod.CtrlCmd | KeyCode.KEY_F - } -}); -KeybindingsRegistry.registerCommandAndKeybindingRule(showNextFindWdigetCommand.toCommandAndKeybindingRule(KeybindingsRegistry.WEIGHT.editorContrib())); - - -const showNextFindTermCommand = new ShowWebViewEditorFindTermCommand({ - id: 'editor.action.webvieweditor.showNextFindTerm', - precondition: KEYBINDING_CONTEXT_WEBVIEWEDITOR_FIND_WIDGET_INPUT_FOCUSED, - kbOpts: { - primary: KeyMod.Alt | KeyCode.DownArrow - } -}, true); -KeybindingsRegistry.registerCommandAndKeybindingRule(showNextFindTermCommand.toCommandAndKeybindingRule(KeybindingsRegistry.WEIGHT.editorContrib())); - -const showPreviousFindTermCommand = new ShowWebViewEditorFindTermCommand({ - id: 'editor.action.webvieweditor.showPreviousFindTerm', - precondition: KEYBINDING_CONTEXT_WEBVIEWEDITOR_FIND_WIDGET_INPUT_FOCUSED, - kbOpts: { - primary: KeyMod.Alt | KeyCode.UpArrow - } -}, false); -KeybindingsRegistry.registerCommandAndKeybindingRule(showPreviousFindTermCommand.toCommandAndKeybindingRule(KeybindingsRegistry.WEIGHT.editorContrib())); - - -const hideCommand = new HideWebViewEditorFindCommand({ - id: HideWebViewEditorFindCommand.Id, - precondition: ContextKeyExpr.and( - KEYBINDING_CONTEXT_WEBVIEWEDITOR_FOCUS, - KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE), - kbOpts: { - primary: KeyCode.Escape - } -}); -KeybindingsRegistry.registerCommandAndKeybindingRule(hideCommand.toCommandAndKeybindingRule(KeybindingsRegistry.WEIGHT.editorContrib())); - - -actionRegistry.registerWorkbenchAction( - new SyncActionDescriptor(OpenWebviewDeveloperToolsAction, OpenWebviewDeveloperToolsAction.ID, OpenWebviewDeveloperToolsAction.LABEL), - 'Webview Tools', - webviewDeveloperCategory); - -actionRegistry.registerWorkbenchAction( - new SyncActionDescriptor(ReloadWebviewAction, ReloadWebviewAction.ID, ReloadWebviewAction.LABEL), - 'Reload Webview', - webviewDeveloperCategory); \ No newline at end of file diff --git a/src/vs/workbench/parts/localizations/browser/localizations.contribution.ts b/src/vs/workbench/parts/localizations/browser/localizations.contribution.ts index 4d111bf50d3..1c89acd7c6f 100644 --- a/src/vs/workbench/parts/localizations/browser/localizations.contribution.ts +++ b/src/vs/workbench/parts/localizations/browser/localizations.contribution.ts @@ -64,17 +64,25 @@ export class LocalizationWorkbenchContribution extends Disposable implements IWo if (!this.storageService.getBoolean(donotAskUpdateKey) && e.local && e.local.manifest.contributes && e.local.manifest.contributes.localizations && e.local.manifest.contributes.localizations.length) { const locale = e.local.manifest.contributes.localizations[0].languageId; if (language !== locale) { - const updateLocaleMessage = localize('updateLocale', "Would you like to change VS Code's UI language to {0} and restart?", e.local.manifest.contributes.localizations[0].languageName || e.local.manifest.contributes.localizations[0].languageId); - this.notificationService.prompt(Severity.Info, updateLocaleMessage, [localize('yes', "Yes"), localize('no', "No"), localize('doNotAskAgain', "Do not ask me again")]) - .then(option => { - if (option === 0) { + this.notificationService.prompt( + Severity.Info, + localize('updateLocale', "Would you like to change VS Code's UI language to {0} and restart?", e.local.manifest.contributes.localizations[0].languageName || e.local.manifest.contributes.localizations[0].languageId), + [{ + label: localize('yes', "Yes"), + run: () => { const file = URI.file(join(this.environmentService.appSettingsHome, 'locale.json')); this.jsonEditingService.write(file, { key: 'locale', value: locale }, true) .then(() => this.windowsService.relaunch({}), e => this.notificationService.error(e)); - } else if (option === 2) { - this.storageService.store(donotAskUpdateKey, true); } - }); + }, { + label: localize('no', "No"), + run: () => { } + }, { + label: localize('neverAgain', "Don't Show Again"), + isSecondary: true, + run: () => this.storageService.store(donotAskUpdateKey, true) + }] + ); } } } diff --git a/src/vs/workbench/parts/markers/electron-browser/constants.ts b/src/vs/workbench/parts/markers/electron-browser/constants.ts index b69836eaca0..88d963e3b64 100644 --- a/src/vs/workbench/parts/markers/electron-browser/constants.ts +++ b/src/vs/workbench/parts/markers/electron-browser/constants.ts @@ -9,8 +9,10 @@ export default { MARKERS_PANEL_ID: 'workbench.panel.markers', MARKER_COPY_ACTION_ID: 'problems.action.copy', MARKER_COPY_MESSAGE_ACTION_ID: 'problems.action.copyMessage', + RELATED_INFORMATION_COPY_MESSAGE_ACTION_ID: 'problems.action.copyRelatedInformationMessage', MARKER_OPEN_SIDE_ACTION_ID: 'problems.action.openToSide', MARKER_SHOW_PANEL_ID: 'workbench.action.showErrorsWarnings', - MarkerFocusContextKey: new RawContextKey('problemFocus', true) + MarkerFocusContextKey: new RawContextKey('problemFocus', true), + RelatedInformationFocusContextKey: new RawContextKey('relatedInformationFocus', true) }; diff --git a/src/vs/workbench/parts/markers/electron-browser/markers.contribution.ts b/src/vs/workbench/parts/markers/electron-browser/markers.contribution.ts index 2c1ecca2c3f..789c79ff77b 100644 --- a/src/vs/workbench/parts/markers/electron-browser/markers.contribution.ts +++ b/src/vs/workbench/parts/markers/electron-browser/markers.contribution.ts @@ -13,7 +13,7 @@ import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/wor import { KeybindingsRegistry, IKeybindings } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { localize } from 'vs/nls'; -import { Marker } from 'vs/workbench/parts/markers/electron-browser/markersModel'; +import { Marker, RelatedInformation } from 'vs/workbench/parts/markers/electron-browser/markersModel'; import { MarkersPanel } from 'vs/workbench/parts/markers/electron-browser/markersPanel'; import { MenuId, MenuRegistry, SyncActionDescriptor } from 'vs/platform/actions/common/actions'; import { PanelRegistry, Extensions as PanelExtensions, PanelDescriptor } from 'vs/workbench/browser/panel'; @@ -101,7 +101,7 @@ registerAction({ }); registerAction({ id: Constants.MARKER_COPY_MESSAGE_ACTION_ID, - title: localize('copyMarkerMessage', "Copy Message"), + title: localize('copyMessage', "Copy Message"), handler(accessor) { copyMessage(accessor.get(IPanelService)); }, @@ -111,6 +111,18 @@ registerAction({ group: 'navigation' } }); +registerAction({ + id: Constants.RELATED_INFORMATION_COPY_MESSAGE_ACTION_ID, + title: localize('copyMessage', "Copy Message"), + handler(accessor) { + copyRelatedInformationMessage(accessor.get(IPanelService)); + }, + menu: { + menuId: MenuId.ProblemsPanelContext, + when: Constants.RelatedInformationFocusContextKey, + group: 'navigation' + } +}); function copyMarker(panelService: IPanelService) { @@ -133,6 +145,16 @@ function copyMessage(panelService: IPanelService) { } } +function copyRelatedInformationMessage(panelService: IPanelService) { + const activePanel = panelService.getActivePanel(); + if (activePanel instanceof MarkersPanel) { + const element = (activePanel).getFocusElement(); + if (element instanceof RelatedInformation) { + clipboard.writeText(element.raw.message); + } + } +} + interface IActionDescriptor { id: string; handler: ICommandHandler; diff --git a/src/vs/workbench/parts/markers/electron-browser/markers.ts b/src/vs/workbench/parts/markers/electron-browser/markers.ts index a15da7a99e9..2c104743afd 100644 --- a/src/vs/workbench/parts/markers/electron-browser/markers.ts +++ b/src/vs/workbench/parts/markers/electron-browser/markers.ts @@ -13,16 +13,26 @@ import Constants from './constants'; import URI from 'vs/base/common/uri'; import { Event, Emitter } from 'vs/base/common/event'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { deepClone, mixin } from 'vs/base/common/objects'; +import { IExpression, getEmptyExpression } from 'vs/base/common/glob'; +import { IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; +import { join, isAbsolute } from 'vs/base/common/paths'; export const IMarkersWorkbenchService = createDecorator('markersWorkbenchService'); +export interface IFilter { + filterText: string; + useFilesExclude: boolean; +} + export interface IMarkersWorkbenchService { _serviceBrand: any; - readonly onDidChangeMarkersForResources: Event; + readonly onDidChange: Event; readonly markersModel: MarkersModel; - filter(filter: string): void; + filter(filter: IFilter): void; } export class MarkersWorkbenchService extends Disposable implements IMarkersWorkbenchService { @@ -30,21 +40,30 @@ export class MarkersWorkbenchService extends Disposable implements IMarkersWorkb readonly markersModel: MarkersModel; - private readonly _onDidChangeMarkersForResources: Emitter = this._register(new Emitter()); - readonly onDidChangeMarkersForResources: Event = this._onDidChangeMarkersForResources.event; + private readonly _onDidChange: Emitter = this._register(new Emitter()); + readonly onDidChange: Event = this._onDidChange.event; + + private useFilesExclude: boolean = false; constructor( @IMarkerService private markerService: IMarkerService, + @IConfigurationService private configurationService: IConfigurationService, + @IWorkspaceContextService private workspaceContextService: IWorkspaceContextService, @IActivityService private activityService: IActivityService ) { super(); this.markersModel = this._register(new MarkersModel(this.readMarkers())); this._register(markerService.onMarkerChanged(resources => this.onMarkerChanged(resources))); + this._register(configurationService.onDidChangeConfiguration(e => { + if (this.useFilesExclude && e.affectsConfiguration('files.exclude')) { + this.doFilter(this.markersModel.filterOptions.filter, this.getExcludeExpression()); + } + })); } - filter(filter: string): void { - this.markersModel.updateFilterOptions(new FilterOptions(filter)); - this.refreshBadge(); + filter(filter: IFilter): void { + this.useFilesExclude = filter.useFilesExclude; + this.doFilter(filter.filterText, this.getExcludeExpression()); } private onMarkerChanged(resources: URI[]): void { @@ -54,19 +73,61 @@ export class MarkersWorkbenchService extends Disposable implements IMarkersWorkb } }); this.refreshBadge(); - this._onDidChangeMarkersForResources.fire(resources); + this._onDidChange.fire(resources); } private readMarkers(resource?: URI): IMarker[] { return this.markerService.read({ resource, severities: MarkerSeverity.Error | MarkerSeverity.Warning | MarkerSeverity.Info }); } + private getExcludeExpression(): IExpression { + if (this.useFilesExclude) { + const workspaceFolders = this.workspaceContextService.getWorkspace().folders; + if (workspaceFolders.length) { + const result = getEmptyExpression(); + for (const workspaceFolder of workspaceFolders) { + mixin(result, this.getExcludesForFolder(workspaceFolder)); + } + return result; + } else { + return this.getFilesExclude(); + } + } + return {}; + } + + private doFilter(filterText: string, filesExclude: IExpression): void { + this.markersModel.updateFilterOptions(new FilterOptions(filterText, filesExclude)); + this.refreshBadge(); + this._onDidChange.fire([]); + } + private refreshBadge(): void { - const { total, filtered } = this.markersModel.stats(); - const message = total === filtered ? localize('totalProblems', 'Total {0} Problems', total) : localize('filteredProblems', 'Showing {0} of {1} Problems', filtered, total); - this.activityService.showActivity(Constants.MARKERS_PANEL_ID, new NumberBadge(filtered, () => message)); + const { total } = this.markersModel.stats(); + const message = localize('totalProblems', 'Total {0} Problems', total); + this.activityService.showActivity(Constants.MARKERS_PANEL_ID, new NumberBadge(total, () => message)); + } + + private getExcludesForFolder(workspaceFolder: IWorkspaceFolder): IExpression { + const expression = this.getFilesExclude(workspaceFolder.uri); + return this.getAbsoluteExpression(expression, workspaceFolder.uri.fsPath); + } + + private getFilesExclude(resource?: URI): IExpression { + return deepClone(this.configurationService.getValue('files.exclude', { resource })) || {}; + } + + private getAbsoluteExpression(expr: IExpression, root: string): IExpression { + return Object.keys(expr) + .reduce((absExpr: IExpression, key: string) => { + if (expr[key] && !isAbsolute(key)) { + const absPattern = join(root, key); + absExpr[absPattern] = expr[key]; + } + + return absExpr; + }, Object.create(null)); } } - -registerSingleton(IMarkersWorkbenchService, MarkersWorkbenchService); +registerSingleton(IMarkersWorkbenchService, MarkersWorkbenchService); \ No newline at end of file diff --git a/src/vs/workbench/parts/markers/electron-browser/markersModel.ts b/src/vs/workbench/parts/markers/electron-browser/markersModel.ts index d0dbabbde75..652e9c08a5b 100644 --- a/src/vs/workbench/parts/markers/electron-browser/markersModel.ts +++ b/src/vs/workbench/parts/markers/electron-browser/markersModel.ts @@ -13,6 +13,8 @@ import Messages from 'vs/workbench/parts/markers/electron-browser/messages'; import { Schemas } from 'vs/base/common/network'; import { groupBy, isFalsyOrEmpty, flatten } from 'vs/base/common/arrays'; import { values } from 'vs/base/common/map'; +import * as glob from 'vs/base/common/glob'; +import * as strings from 'vs/base/common/strings'; function compareUris(a: URI, b: URI) { if (a.toString() < b.toString()) { @@ -34,7 +36,9 @@ export class ResourceMarkers extends NodeWithId { private _path: string = null; readonly markers: Marker[]; - filteredCount: number = 0; + isExcluded: boolean = false; + isIncluded: boolean = false; + filteredCount: number; uriMatches: IMatch[] = []; constructor( @@ -96,14 +100,11 @@ export class Marker extends NodeWithId { } public toString(): string { - return [ - `file: '${this.raw.resource}'`, - `severity: '${MarkerSeverity.toString(this.raw.severity)}'`, - `message: '${this.raw.message}'`, - `at: '${this.raw.startLineNumber},${this.raw.startColumn}'`, - `source: '${this.raw.source ? this.raw.source : ''}'`, - `code: '${this.raw.code ? this.raw.code : ''}'` - ].join('\n'); + return JSON.stringify({ + ...this.raw, + resource: this.raw.resource.path, + relatedInformation: this.resourceRelatedInformation.length ? this.resourceRelatedInformation.map(r => ({ ...r.raw, resource: r.raw.resource.path })) : void 0 + }, null, '\t'); } static compare(a: Marker, b: Marker): number { @@ -130,21 +131,48 @@ export class FilterOptions { readonly filterErrors: boolean = false; readonly filterWarnings: boolean = false; readonly filterInfos: boolean = false; - readonly filter: string = ''; - readonly completeFilter: string = ''; + readonly excludePattern: glob.ParsedExpression = null; + readonly includePattern: glob.ParsedExpression = null; + readonly textFilter: string = ''; - constructor(filter: string = '') { - if (filter) { - this.completeFilter = filter; - this.filter = filter.trim(); - this.filterErrors = this.matches(this.filter, Messages.MARKERS_PANEL_FILTER_ERRORS); - this.filterWarnings = this.matches(this.filter, Messages.MARKERS_PANEL_FILTER_WARNINGS); - this.filterInfos = this.matches(this.filter, Messages.MARKERS_PANEL_FILTER_INFOS); + constructor(readonly filter: string = '', excludePatterns: glob.IExpression = {}) { + filter = filter.trim(); + for (const key of Object.keys(excludePatterns)) { + if (excludePatterns[key]) { + this.setPattern(excludePatterns, key); + } + delete excludePatterns[key]; } + const includePatterns: glob.IExpression = glob.getEmptyExpression(); + if (filter) { + const filters = glob.splitGlobAware(filter, ',').map(s => s.trim()).filter(s => !!s.length); + for (const f of filters) { + this.filterErrors = this.filterErrors || this.matches(f, Messages.MARKERS_PANEL_FILTER_ERRORS); + this.filterWarnings = this.filterWarnings || this.matches(f, Messages.MARKERS_PANEL_FILTER_WARNINGS); + this.filterInfos = this.filterInfos || this.matches(f, Messages.MARKERS_PANEL_FILTER_INFOS); + if (strings.startsWith(f, '!')) { + this.setPattern(excludePatterns, strings.ltrim(f, '!')); + } else { + this.setPattern(includePatterns, f); + this.textFilter += ` ${f}`; + } + } + } + if (Object.keys(excludePatterns).length) { + this.excludePattern = glob.parse(excludePatterns); + } + if (Object.keys(includePatterns).length) { + this.includePattern = glob.parse(includePatterns); + } + this.textFilter = this.textFilter.trim(); } - public hasFilters(): boolean { - return !!this.filter; + private setPattern(expression: glob.IExpression, pattern: string) { + if (pattern[0] === '.') { + pattern = '*' + pattern; // convert ".js" to "*.js" + } + expression[`**/${pattern}/**`] = true; + expression[`**/${pattern}`] = true; } private matches(prefix: string, word: string): boolean { @@ -232,89 +260,90 @@ export class MarkersModel { public updateFilterOptions(filterOptions: FilterOptions): void { this._filterOptions = filterOptions; - if (!this._filterOptions.hasFilters()) { - // reset all filters/matches - this._markersByResource.forEach(resource => { - resource.filteredCount = resource.markers.length; - resource.uriMatches = []; - - for (const marker of resource.markers) { - marker.isSelected = true; - marker.messageMatches = []; - marker.sourceMatches = []; - marker.resourceRelatedInformation.forEach(r => { - r.uriMatches = []; - r.messageMatches = []; - }); - } - }); - } else { - // update properly - this._markersByResource.forEach(resource => { - - resource.uriMatches = this._filterOptions.hasFilters() ? FilterOptions._filter(this._filterOptions.filter, paths.basename(resource.uri.fsPath)) : []; - resource.filteredCount = 0; - - for (const marker of resource.markers) { - marker.messageMatches = this._filterOptions.hasFilters() ? FilterOptions._fuzzyFilter(this._filterOptions.filter, marker.raw.message) : []; - marker.sourceMatches = marker.raw.source && this._filterOptions.hasFilters() ? FilterOptions._filter(this._filterOptions.filter, marker.raw.source) : []; - marker.isSelected = this.filterMarker(marker.raw); - if (marker.isSelected) { - resource.filteredCount += 1; - } - marker.resourceRelatedInformation.forEach(r => { - r.uriMatches = this._filterOptions.hasFilters() ? FilterOptions._filter(this._filterOptions.filter, paths.basename(r.raw.resource.fsPath)) : []; - r.messageMatches = this._filterOptions.hasFilters() ? FilterOptions._fuzzyFilter(this._filterOptions.filter, r.raw.message) : []; - }); - } - }); - } + this._markersByResource.forEach(resource => { + this.updateResource(resource); + for (const marker of resource.markers) { + this.updateMarker(marker, resource); + } + this.updateFilteredCount(resource); + }); } private createResource(uri: URI, rawMarkers: IMarker[]): ResourceMarkers { let markers: Marker[] = []; - let filteredCount = 0; - for (let i = 0; i < rawMarkers.length; i++) { - let marker = this.createMarker(rawMarkers[i], i, uri.toString()); - markers.push(marker); - if (marker.isSelected) { - filteredCount += 1; - } - } - const resource = new ResourceMarkers(uri, markers); - resource.filteredCount = filteredCount; - resource.uriMatches = this._filterOptions.hasFilters() ? FilterOptions._filter(this._filterOptions.filter, paths.basename(uri.fsPath)) : []; + this.updateResource(resource); + + rawMarkers.forEach((rawMarker, index) => { + const marker = new Marker(uri.toString() + index, rawMarker); + if (rawMarker.relatedInformation) { + const groupedByResource = groupBy(rawMarker.relatedInformation, MarkersModel._compareMarkersByUri); + groupedByResource.sort((a, b) => compareUris(a[0].resource, b[0].resource)); + marker.resourceRelatedInformation = flatten(groupedByResource).map((r, index) => new RelatedInformation(marker.id + index, r)); + } + this.updateMarker(marker, resource); + markers.push(marker); + }); + + this.updateFilteredCount(resource); return resource; } - private createMarker(rawMarker: IMarker, index: number, uri: string): Marker { - const marker = new Marker(uri + index, rawMarker); - marker.messageMatches = this._filterOptions.hasFilters() ? FilterOptions._fuzzyFilter(this._filterOptions.filter, rawMarker.message) : []; - marker.sourceMatches = rawMarker.source && this._filterOptions.hasFilters() ? FilterOptions._filter(this._filterOptions.filter, rawMarker.source) : []; - marker.isSelected = this.filterMarker(rawMarker); - if (rawMarker.relatedInformation) { - const groupedByResource = groupBy(rawMarker.relatedInformation, MarkersModel._compareMarkersByUri); - groupedByResource.sort((a, b) => compareUris(a[0].resource, b[0].resource)); - marker.resourceRelatedInformation = flatten(groupedByResource).map((r, index) => { - const relatedInformation = new RelatedInformation(marker.id + index, r); - relatedInformation.uriMatches = this._filterOptions.hasFilters() ? FilterOptions._filter(this._filterOptions.filter, paths.basename(r.resource.fsPath)) : []; - relatedInformation.messageMatches = this._filterOptions.hasFilters() ? FilterOptions._fuzzyFilter(this._filterOptions.filter, r.message) : []; - return relatedInformation; - }); - } - return marker; + private updateResource(resource: ResourceMarkers): void { + resource.isExcluded = this.isResourceExcluded(resource); + resource.isIncluded = this.isResourceIncluded(resource); + resource.uriMatches = this._filterOptions.textFilter ? FilterOptions._filter(this._filterOptions.textFilter, paths.basename(resource.uri.fsPath)) : []; } - private filterMarker(marker: IMarker): boolean { - if (!this._filterOptions.hasFilters()) { + private updateFilteredCount(resource: ResourceMarkers): void { + if (resource.isExcluded) { + resource.filteredCount = 0; + } else if (resource.isIncluded) { + resource.filteredCount = resource.markers.length; + } else { + resource.filteredCount = resource.markers.filter(m => m.isSelected).length; + } + } + + private updateMarker(marker: Marker, resource: ResourceMarkers): void { + marker.messageMatches = !resource.isExcluded && this._filterOptions.textFilter ? FilterOptions._fuzzyFilter(this._filterOptions.textFilter, marker.raw.message) : []; + marker.sourceMatches = !resource.isExcluded && marker.raw.source && this._filterOptions.textFilter ? FilterOptions._filter(this._filterOptions.textFilter, marker.raw.source) : []; + marker.resourceRelatedInformation.forEach(r => { + r.uriMatches = !resource.isExcluded && this._filterOptions.textFilter ? FilterOptions._filter(this._filterOptions.textFilter, paths.basename(r.raw.resource.fsPath)) : []; + r.messageMatches = !resource.isExcluded && this._filterOptions.textFilter ? FilterOptions._fuzzyFilter(this._filterOptions.textFilter, r.raw.message) : []; + }); + marker.isSelected = this.isMarkerSelected(marker.raw, resource); + } + + private isResourceExcluded(resource: ResourceMarkers): boolean { + if (resource.uri.scheme === Schemas.walkThrough || resource.uri.scheme === Schemas.walkThroughSnippet) { return true; } - if (marker.resource.scheme === Schemas.walkThrough || marker.resource.scheme === Schemas.walkThroughSnippet) { + if (this.filterOptions.excludePattern && !!this.filterOptions.excludePattern(resource.uri.fsPath)) { + return true; + } + return false; + } + + private isResourceIncluded(resource: ResourceMarkers): boolean { + if (this.filterOptions.includePattern && this.filterOptions.includePattern(resource.uri.fsPath)) { + return true; + } + if (this._filterOptions.textFilter && !!FilterOptions._filter(this._filterOptions.textFilter, paths.basename(resource.uri.fsPath))) { + return true; + } + return false; + } + + private isMarkerSelected(marker: IMarker, resource: ResourceMarkers): boolean { + if (resource.isExcluded) { return false; } + if (resource.isIncluded) { + return true; + } if (this._filterOptions.filterErrors && MarkerSeverity.Error === marker.severity) { return true; } @@ -324,18 +353,18 @@ export class MarkersModel { if (this._filterOptions.filterInfos && MarkerSeverity.Info === marker.severity) { return true; } - if (!!FilterOptions._fuzzyFilter(this._filterOptions.filter, marker.message)) { + if (!this._filterOptions.textFilter) { return true; } - if (!!FilterOptions._filter(this._filterOptions.filter, paths.basename(marker.resource.fsPath))) { + if (!!FilterOptions._fuzzyFilter(this._filterOptions.textFilter, marker.message)) { return true; } - if (!!marker.source && !!FilterOptions._filter(this._filterOptions.filter, marker.source)) { + if (!!marker.source && !!FilterOptions._filter(this._filterOptions.textFilter, marker.source)) { return true; } if (!!marker.relatedInformation && marker.relatedInformation.some(r => - !!FilterOptions._filter(this._filterOptions.filter, paths.basename(r.resource.fsPath)) || ! - !FilterOptions._filter(this._filterOptions.filter, r.message))) { + !!FilterOptions._filter(this._filterOptions.textFilter, paths.basename(r.resource.fsPath)) || ! + !FilterOptions._filter(this._filterOptions.textFilter, r.message))) { return true; } return false; @@ -344,16 +373,4 @@ export class MarkersModel { public dispose(): void { this._markersByResource.clear(); } - - public getMessage(): string { - if (this.hasFilteredResources()) { - return ''; - } - if (this.hasResources()) { - if (this._filterOptions.hasFilters()) { - return Messages.MARKERS_PANEL_NO_PROBLEMS_FILTERS; - } - } - return Messages.MARKERS_PANEL_NO_PROBLEMS_BUILT; - } } diff --git a/src/vs/workbench/parts/markers/electron-browser/markersPanel.ts b/src/vs/workbench/parts/markers/electron-browser/markersPanel.ts index 913ce14a873..7c11d749c64 100644 --- a/src/vs/workbench/parts/markers/electron-browser/markersPanel.ts +++ b/src/vs/workbench/parts/markers/electron-browser/markersPanel.ts @@ -10,9 +10,7 @@ import URI from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import { Delayer } from 'vs/base/common/async'; import * as dom from 'vs/base/browser/dom'; -import * as builder from 'vs/base/browser/builder'; -import { IAction, Action } from 'vs/base/common/actions'; -import { IActionItem } from 'vs/base/browser/ui/actionbar/actionbar'; +import { IAction, IActionItem } from 'vs/base/common/actions'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService'; import { Panel } from 'vs/workbench/browser/panel'; @@ -22,7 +20,7 @@ import { Marker, ResourceMarkers, RelatedInformation } from 'vs/workbench/parts/ import { Controller } from 'vs/workbench/parts/markers/electron-browser/markersTreeController'; import * as Viewer from 'vs/workbench/parts/markers/electron-browser/markersTreeViewer'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { CollapseAllAction, FilterAction, FilterInputBoxActionItem } from 'vs/workbench/parts/markers/electron-browser/markersPanelActions'; +import { CollapseAllAction, MarkersFilterActionItem, MarkersFilterAction } from 'vs/workbench/parts/markers/electron-browser/markersPanelActions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import Messages from 'vs/workbench/parts/markers/electron-browser/messages'; import { RangeHighlightDecorations } from 'vs/workbench/browser/parts/editor/rangeDecorations'; @@ -32,6 +30,9 @@ import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { TreeResourceNavigator, WorkbenchTree } from 'vs/platform/list/browser/listService'; import { IMarkersWorkbenchService } from 'vs/workbench/parts/markers/electron-browser/markers'; import { SimpleFileResourceDragAndDrop } from 'vs/workbench/browser/dnd'; +import { IStorageService } from 'vs/platform/storage/common/storage'; +import { Scope } from 'vs/workbench/common/memento'; +import { localize } from 'vs/nls'; export class MarkersPanel extends Panel { @@ -45,12 +46,12 @@ export class MarkersPanel extends Panel { private rangeHighlightDecorations: RangeHighlightDecorations; private actions: IAction[]; - private filterAction: FilterAction; private collapseAllAction: IAction; + private filterInputActionItem: MarkersFilterActionItem; private treeContainer: HTMLElement; private messageBoxContainer: HTMLElement; - private messageBox: HTMLElement; + private panelSettings: any; private currentResourceGotAddedToMarkersData: boolean = false; @@ -61,28 +62,32 @@ export class MarkersPanel extends Panel { @IConfigurationService private configurationService: IConfigurationService, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, - @IMarkersWorkbenchService private markersWorkbenchService: IMarkersWorkbenchService + @IMarkersWorkbenchService private markersWorkbenchService: IMarkersWorkbenchService, + @IStorageService storageService: IStorageService, ) { super(Constants.MARKERS_PANEL_ID, telemetryService, themeService); this.delayedRefresh = new Delayer(500); this.autoExpanded = new Set(); + this.panelSettings = this.getMemento(storageService, Scope.WORKSPACE); } - public create(parent: builder.Builder): TPromise { + public create(parent: HTMLElement): TPromise { super.create(parent); this.rangeHighlightDecorations = this.instantiationService.createInstance(RangeHighlightDecorations); this.toUnbind.push(this.rangeHighlightDecorations); - dom.addClass(parent.getHTMLElement(), 'markers-panel'); + dom.addClass(parent, 'markers-panel'); - let container = dom.append(parent.getHTMLElement(), dom.$('.markers-panel-container')); + let container = dom.append(parent, dom.$('.markers-panel-container')); this.createMessageBox(container); this.createTree(container); - + this.createActions(); this.createListeners(); + this.updateFilter(); + return this.render(); } @@ -90,8 +95,10 @@ export class MarkersPanel extends Panel { return Messages.MARKERS_PANEL_TITLE_PROBLEMS; } - public layout(dimension: builder.Dimension): void { - this.tree.layout(dimension.height); + public layout(dimension: dom.Dimension): void { + this.treeContainer.style.height = `${dimension.height}px`; + this.tree.layout(dimension.height, dimension.width); + this.filterInputActionItem.toggleLayout(dimension.width < 1200); } public focus(): void { @@ -107,7 +114,7 @@ export class MarkersPanel extends Panel { this.highlightCurrentSelectedMarkerRange(); this.autoReveal(true); } else { - this.messageBox.focus(); + this.messageBoxContainer.focus(); } } @@ -127,9 +134,8 @@ export class MarkersPanel extends Panel { public getActions(): IAction[] { if (!this.actions) { - this.actions = this.createActions(); + this.createActions(); } - this.collapseAllAction.enabled = this.markersWorkbenchService.markersModel.hasFilteredResources(); return this.actions; } @@ -173,17 +179,13 @@ export class MarkersPanel extends Panel { return TPromise.as(null); } - public updateFilter(filter: string) { - this.markersWorkbenchService.filter(filter); + private updateFilter() { this.autoExpanded = new Set(); - this.refreshPanel(); - this.autoReveal(); + this.markersWorkbenchService.filter({ filterText: this.filterInputActionItem.getFilterText(), useFilesExclude: this.filterInputActionItem.useFilesExclude }); } private createMessageBox(parent: HTMLElement): void { this.messageBoxContainer = dom.append(parent, dom.$('.message-box-container')); - this.messageBox = dom.append(this.messageBoxContainer, dom.$('span')); - this.messageBox.setAttribute('tabindex', '0'); } private createTree(parent: HTMLElement): void { @@ -204,7 +206,17 @@ export class MarkersPanel extends Panel { ariaLabel: Messages.MARKERS_PANEL_ARIA_LABEL_PROBLEMS_TREE }); - Constants.MarkerFocusContextKey.bindTo(this.tree.contextKeyService); + const markerFocusContextKey = Constants.MarkerFocusContextKey.bindTo(this.tree.contextKeyService); + const relatedInformationFocusContextKey = Constants.RelatedInformationFocusContextKey.bindTo(this.tree.contextKeyService); + this._register(this.tree.onDidChangeFocus((e: { focus: any }) => { + markerFocusContextKey.set(e.focus instanceof Marker); + relatedInformationFocusContextKey.set(e.focus instanceof RelatedInformation); + })); + const focusTracker = this._register(dom.trackFocus(this.tree.getHTMLElement())); + this._register(focusTracker.onDidBlur(() => { + markerFocusContextKey.set(false); + relatedInformationFocusContextKey.set(false); + })); const markersNavigator = this._register(new TreeResourceNavigator(this.tree, { openOnFocus: true })); this._register(debounceEvent(markersNavigator.openResource, (last, event) => event, 75, true)(options => { @@ -212,28 +224,24 @@ export class MarkersPanel extends Panel { })); } - private createActions(): IAction[] { + private createActions(): void { this.collapseAllAction = this.instantiationService.createInstance(CollapseAllAction, this.tree, true); - this.filterAction = new FilterAction(); - const actions = [ - this.filterAction, - this.collapseAllAction - ]; - actions.forEach(a => { - this.toUnbind.push(a); - }); - return actions; + const filterAction = this.instantiationService.createInstance(MarkersFilterAction); + this.filterInputActionItem = this.instantiationService.createInstance(MarkersFilterActionItem, { filterText: this.panelSettings['filter'] || '', filterHistory: this.panelSettings['filterHistory'] || [], useFilesExclude: !!this.panelSettings['useFilesExclude'] }, filterAction); + this.actions = [filterAction, this.collapseAllAction]; } private createListeners(): void { - this.toUnbind.push(this.markersWorkbenchService.onDidChangeMarkersForResources(this.onMarkerChanged, this)); + this.toUnbind.push(this.markersWorkbenchService.onDidChange(resources => this.onDidChange(resources))); this.toUnbind.push(this.editorGroupService.onEditorsChanged(this.onEditorsChanged, this)); this.toUnbind.push(this.tree.onDidChangeSelection(() => this.onSelected())); + this.toUnbind.push(this.filterInputActionItem.onDidChange(() => this.updateFilter())); + this.actions.forEach(a => this.toUnbind.push(a)); } - private onMarkerChanged(changedResources: URI[]) { - this.currentResourceGotAddedToMarkersData = this.currentResourceGotAddedToMarkersData || this.isCurrentResourceGotAddedToMarkersData(changedResources); - this.updateResources(changedResources); + private onDidChange(resources: URI[]) { + this.currentResourceGotAddedToMarkersData = this.currentResourceGotAddedToMarkersData || this.isCurrentResourceGotAddedToMarkersData(resources); + this.updateResources(resources); this.delayedRefresh.trigger(() => { this.refreshPanel(); this.updateRangeHighlights(); @@ -286,8 +294,44 @@ export class MarkersPanel extends Panel { } private renderMessage(): void { - this.messageBox.textContent = this.markersWorkbenchService.markersModel.getMessage(); - dom.toggleClass(this.messageBoxContainer, 'hidden', this.markersWorkbenchService.markersModel.hasFilteredResources()); + const markersModel = this.markersWorkbenchService.markersModel; + const hasFilteredResources = markersModel.hasFilteredResources(); + dom.clearNode(this.messageBoxContainer); + dom.toggleClass(this.messageBoxContainer, 'hidden', hasFilteredResources); + if (!hasFilteredResources) { + if (markersModel.hasResources()) { + if (markersModel.filterOptions.filter) { + this.renderFilteredByFilterMessage(this.messageBoxContainer); + } else { + this.renderFilteredByFilesExcludeMessage(this.messageBoxContainer); + } + } else { + this.renderNoProblemsMessage(this.messageBoxContainer); + } + } + } + + private renderFilteredByFilesExcludeMessage(container: HTMLElement) { + const span1 = dom.append(container, dom.$('span')); + span1.textContent = Messages.MARKERS_PANEL_NO_PROBLEMS_FILE_EXCLUSIONS_FILTER; + const link = dom.append(container, dom.$('a.messageAction')); + link.textContent = localize('disableFilesExclude', "Disable Files Exclude."); + link.setAttribute('tabIndex', '0'); + dom.addDisposableListener(link, dom.EventType.CLICK, () => this.filterInputActionItem.useFilesExclude = false); + } + + private renderFilteredByFilterMessage(container: HTMLElement) { + const span1 = dom.append(container, dom.$('span')); + span1.textContent = Messages.MARKERS_PANEL_NO_PROBLEMS_FILTERS; + const link = dom.append(container, dom.$('a.messageAction')); + link.textContent = localize('clearFilter', "Clear Filter."); + link.setAttribute('tabIndex', '0'); + dom.addDisposableListener(link, dom.EventType.CLICK, () => this.filterInputActionItem.clear()); + } + + private renderNoProblemsMessage(container: HTMLElement) { + const span = dom.append(container, dom.$('span')); + span.textContent = Messages.MARKERS_PANEL_NO_PROBLEMS_BUILT; } private autoExpand(): void { @@ -366,15 +410,24 @@ export class MarkersPanel extends Panel { } } - public getActionItem(action: Action): IActionItem { - if (action.id === FilterAction.ID) { - return this.instantiationService.createInstance(FilterInputBoxActionItem, this, action); + public getFocusElement(): ResourceMarkers | Marker { + return this.tree.getFocus(); + } + + public getActionItem(action: IAction): IActionItem { + if (action.id === MarkersFilterAction.ID) { + return this.filterInputActionItem; } return super.getActionItem(action); } - public getFocusElement(): ResourceMarkers | Marker { - return this.tree.getFocus(); + public shutdown(): void { + // store memento + this.panelSettings['filter'] = this.filterInputActionItem.getFilterText(); + this.panelSettings['filterHistory'] = this.filterInputActionItem.getFilterHistory(); + this.panelSettings['useFilesExclude'] = this.filterInputActionItem.useFilesExclude; + + super.shutdown(); } public dispose(): void { diff --git a/src/vs/workbench/parts/markers/electron-browser/markersPanelActions.ts b/src/vs/workbench/parts/markers/electron-browser/markersPanelActions.ts index 29fa7d076cc..074b2b70348 100644 --- a/src/vs/workbench/parts/markers/electron-browser/markersPanelActions.ts +++ b/src/vs/workbench/parts/markers/electron-browser/markersPanelActions.ts @@ -5,10 +5,8 @@ import { Delayer } from 'vs/base/common/async'; import * as DOM from 'vs/base/browser/dom'; -import * as lifecycle from 'vs/base/common/lifecycle'; import { TPromise } from 'vs/base/common/winjs.base'; -import { IAction, Action } from 'vs/base/common/actions'; -import { BaseActionItem } from 'vs/base/browser/ui/actionbar/actionbar'; +import { Action, IAction } from 'vs/base/common/actions'; import { InputBox } from 'vs/base/browser/ui/inputbox/inputBox'; import { KeyCode } from 'vs/base/common/keyCodes'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; @@ -16,15 +14,21 @@ import { IContextViewService } from 'vs/platform/contextview/browser/contextView import { TogglePanelAction } from 'vs/workbench/browser/panel'; import Messages from 'vs/workbench/parts/markers/electron-browser/messages'; import Constants from 'vs/workbench/parts/markers/electron-browser/constants'; -import { MarkersPanel } from 'vs/workbench/parts/markers/electron-browser/markersPanel'; import { IPartService } from 'vs/workbench/services/part/common/partService'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { CollapseAllAction as TreeCollapseAction } from 'vs/base/parts/tree/browser/treeDefaults'; import * as Tree from 'vs/base/parts/tree/browser/tree'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { attachInputBoxStyler } from 'vs/platform/theme/common/styler'; +import { attachInputBoxStyler, attachStylerCallback, attachCheckboxStyler } from 'vs/platform/theme/common/styler'; import { IMarkersWorkbenchService } from 'vs/workbench/parts/markers/electron-browser/markers'; +import { Event, Emitter } from 'vs/base/common/event'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import { HistoryNavigator } from 'vs/base/common/history'; +import { BaseActionItem } from 'vs/base/browser/ui/actionbar/actionbar'; +import { badgeBackground, contrastBorder } from 'vs/platform/theme/common/colorRegistry'; +import { localize } from 'vs/nls'; +import { Checkbox } from 'vs/base/browser/ui/checkbox/checkbox'; export class ToggleMarkersPanelAction extends TogglePanelAction { @@ -34,8 +38,10 @@ export class ToggleMarkersPanelAction extends TogglePanelAction { constructor(id: string, label: string, @IPartService partService: IPartService, @IPanelService panelService: IPanelService, + @IMarkersWorkbenchService markersWorkbenchService: IMarkersWorkbenchService ) { super(id, label, Constants.MARKERS_PANEL_ID, panelService, partService); + this.enabled = markersWorkbenchService.markersModel.hasFilteredResources(); } } @@ -62,51 +68,211 @@ export class CollapseAllAction extends TreeCollapseAction { } } -export class FilterAction extends Action { +export class MarkersFilterAction extends Action { public static readonly ID: string = 'workbench.actions.problems.filter'; constructor() { - super(FilterAction.ID, Messages.MARKERS_PANEL_ACTION_TOOLTIP_FILTER, 'markers-panel-action-filter', true); + super(MarkersFilterAction.ID, Messages.MARKERS_PANEL_ACTION_TOOLTIP_FILTER, 'markers-panel-action-filter', true); } } -export class FilterInputBoxActionItem extends BaseActionItem { - protected toDispose: lifecycle.IDisposable[]; +export interface IMarkersFilterActionItemOptions { + filterText: string; + filterHistory: string[]; + useFilesExclude: boolean; +} + +export class MarkersFilterActionItem extends BaseActionItem { + + private _toDispose: IDisposable[] = []; + + private readonly _onDidChange: Emitter = this._register(new Emitter()); + readonly onDidChange: Event = this._onDidChange.event; private delayedFilterUpdate: Delayer; + private container: HTMLElement; + private filterInputBox: InputBox; + private filterHistory: HistoryNavigator; + private controlsContainer: HTMLInputElement; + private filterBadge: HTMLInputElement; + private filesExcludeFilter: Checkbox; - constructor(private markersPanel: MarkersPanel, action: IAction, + constructor( + private itemOptions: IMarkersFilterActionItemOptions, + action: IAction, @IContextViewService private contextViewService: IContextViewService, @IThemeService private themeService: IThemeService, @IMarkersWorkbenchService private markersWorkbenchService: IMarkersWorkbenchService, - @ITelemetryService private telemetryService: ITelemetryService) { - super(markersPanel, action); - this.toDispose = []; + @ITelemetryService private telemetryService: ITelemetryService + ) { + super(null, action); this.delayedFilterUpdate = new Delayer(500); + this.filterHistory = new HistoryNavigator(itemOptions.filterHistory || []); } - public render(container: HTMLElement): void { - DOM.addClass(container, 'markers-panel-action-filter'); - let filterInputBox = new InputBox(container, this.contextViewService, { + render(container: HTMLElement): void { + this.container = container; + DOM.addClass(this.container, 'markers-panel-action-filter'); + this.createInput(this.container); + this.createControls(this.container); + this.adjustInputBox(); + } + + clear(): void { + this.filterInputBox.value = ''; + } + + getFilterText(): string { + return this.filterInputBox ? this.filterInputBox.value : this.itemOptions.filterText; + } + + getFilterHistory(): string[] { + return this.filterHistory.getHistory(); + } + + get useFilesExclude(): boolean { + return this.filesExcludeFilter ? this.filesExcludeFilter.checked : this.itemOptions.useFilesExclude; + } + + set useFilesExclude(useFilesExclude: boolean) { + if (this.filesExcludeFilter) { + if (this.filesExcludeFilter.checked !== useFilesExclude) { + this.filesExcludeFilter.checked = useFilesExclude; + this._onDidChange.fire(); + } + } + } + + toggleLayout(small: boolean) { + if (this.container) { + DOM.toggleClass(this.container, 'small', small); + DOM.toggleClass(this.filterBadge, 'small', small); + } + } + + private createInput(container: HTMLElement): void { + this.filterInputBox = new InputBox(container, this.contextViewService, { placeholder: Messages.MARKERS_PANEL_FILTER_PLACEHOLDER, - ariaLabel: Messages.MARKERS_PANEL_FILTER_PLACEHOLDER + ariaLabel: Messages.MARKERS_PANEL_FILTER_ARIA_LABEL }); - this.toDispose.push(attachInputBoxStyler(filterInputBox, this.themeService)); - filterInputBox.value = this.markersWorkbenchService.markersModel.filterOptions.completeFilter; - this.toDispose.push(filterInputBox.onDidChange(filter => this.delayedFilterUpdate.trigger(() => this.updateFilter(filter)))); - this.toDispose.push(DOM.addStandardDisposableListener(filterInputBox.inputElement, 'keyup', (keyboardEvent) => this.onInputKeyUp(keyboardEvent, filterInputBox))); - this.toDispose.push(DOM.addStandardDisposableListener(container, 'keydown', this.handleKeyboardEvent)); - this.toDispose.push(DOM.addStandardDisposableListener(container, 'keyup', this.handleKeyboardEvent)); + this._register(attachInputBoxStyler(this.filterInputBox, this.themeService)); + this.filterInputBox.value = this.itemOptions.filterText; + this._register(this.filterInputBox.onDidChange(filter => this.delayedFilterUpdate.trigger(() => this.onDidInputChange()))); + this._register(DOM.addStandardDisposableListener(this.filterInputBox.inputElement, 'keydown', (keyboardEvent) => this.onInputKeyDown(keyboardEvent, this.filterInputBox))); + this._register(DOM.addStandardDisposableListener(container, 'keydown', this.handleKeyboardEvent)); + this._register(DOM.addStandardDisposableListener(container, 'keyup', this.handleKeyboardEvent)); } - private updateFilter(filter: string) { - this.markersPanel.updateFilter(filter); + private createControls(container: HTMLElement): void { + this.controlsContainer = DOM.append(container, DOM.$('.markers-panel-filter-controls')); + this.createBadge(this.controlsContainer); + this.createFilesExcludeCheckbox(this.controlsContainer); + } + + private createBadge(container: HTMLElement): void { + this.filterBadge = DOM.append(container, DOM.$('.markers-panel-filter-badge')); + this._register(attachStylerCallback(this.themeService, { badgeBackground, contrastBorder }, colors => { + const background = colors.badgeBackground ? colors.badgeBackground.toString() : null; + const border = colors.contrastBorder ? colors.contrastBorder.toString() : null; + + this.filterBadge.style.backgroundColor = background; + + this.filterBadge.style.borderWidth = border ? '1px' : null; + this.filterBadge.style.borderStyle = border ? 'solid' : null; + this.filterBadge.style.borderColor = border; + })); + this.updateBadge(); + this._register(this.markersWorkbenchService.onDidChange(() => this.updateBadge())); + } + + private createFilesExcludeCheckbox(container: HTMLElement): void { + this.filesExcludeFilter = new Checkbox({ + actionClassName: 'markers-panel-filter-filesExclude', + title: this.itemOptions.useFilesExclude ? Messages.MARKERS_PANEL_ACTION_TOOLTIP_DO_NOT_USE_FILES_EXCLUDE : Messages.MARKERS_PANEL_ACTION_TOOLTIP_USE_FILES_EXCLUDE, + isChecked: this.itemOptions.useFilesExclude, + onChange: () => { + this.filesExcludeFilter.domNode.title = this.filesExcludeFilter.checked ? Messages.MARKERS_PANEL_ACTION_TOOLTIP_DO_NOT_USE_FILES_EXCLUDE : Messages.MARKERS_PANEL_ACTION_TOOLTIP_USE_FILES_EXCLUDE; + this._onDidChange.fire(); + } + }); + this._register(attachCheckboxStyler(this.filesExcludeFilter, this.themeService)); + container.appendChild(this.filesExcludeFilter.domNode); + } + + private onDidInputChange() { + const filterText = this.filterInputBox.value; + if (filterText && filterText !== this.filterHistory.current()) { + this.filterHistory.add(this.getFilterText()); + } + this._onDidChange.fire(); this.reportFilteringUsed(); } + private updateBadge(): void { + const { total, filtered } = this.markersWorkbenchService.markersModel.stats(); + DOM.toggleClass(this.filterBadge, 'hidden', total === filtered || filtered === 0); + this.filterBadge.textContent = localize('showing filtered problems', "Showing {0} of {1}", filtered, total); + this.adjustInputBox(); + } + + private adjustInputBox(): void { + this.filterInputBox.inputElement.style.paddingRight = (DOM.getTotalWidth(this.controlsContainer) || 20) + 'px'; + } + + // Action toolbar is swallowing some keys for action items which should not be for an input box + private handleKeyboardEvent(e: IKeyboardEvent) { + switch (e.keyCode) { + case KeyCode.Space: + case KeyCode.LeftArrow: + case KeyCode.RightArrow: + case KeyCode.Escape: + e.stopPropagation(); + break; + } + } + + private showNextFilter() { + let next = this.filterHistory.next(); + if (next) { + this.filterInputBox.value = next; + } + } + + private showPreviousFilter() { + let previous = this.filterHistory.previous(); + if (this.filterInputBox.value) { + this.filterHistory.addIfNotPresent(this.filterInputBox.value); + } + if (previous) { + this.filterInputBox.value = previous; + } + } + + private onInputKeyDown(keyboardEvent: IKeyboardEvent, filterInputBox: InputBox) { + let handled = false; + switch (keyboardEvent.keyCode) { + case KeyCode.Escape: + filterInputBox.value = ''; + handled = true; + break; + case KeyCode.UpArrow: + this.showPreviousFilter(); + handled = true; + break; + case KeyCode.DownArrow: + this.showNextFilter(); + handled = true; + break; + } + if (handled) { + keyboardEvent.stopPropagation(); + keyboardEvent.preventDefault(); + } + } + private reportFilteringUsed(): void { let data = {}; data['errors'] = this.markersWorkbenchService.markersModel.filterOptions.filterErrors; @@ -122,30 +288,8 @@ export class FilterInputBoxActionItem extends BaseActionItem { this.telemetryService.publicLog('problems.filter', data); } - public dispose(): void { - this.toDispose = lifecycle.dispose(this.toDispose); - super.dispose(); + private _register(t: T): T { + this._toDispose.push(t); + return t; } - - // Action toolbar is swallowing some keys for action items which should not be for an input box - private handleKeyboardEvent(e: IKeyboardEvent) { - switch (e.keyCode) { - case KeyCode.Space: - case KeyCode.LeftArrow: - case KeyCode.RightArrow: - case KeyCode.Escape: - e.stopPropagation(); - break; - } - } - - private onInputKeyUp(keyboardEvent: IKeyboardEvent, filterInputBox: InputBox) { - switch (keyboardEvent.keyCode) { - case KeyCode.Escape: - filterInputBox.value = ''; - return; - default: - return; - } - } -} +} \ No newline at end of file diff --git a/src/vs/workbench/parts/markers/electron-browser/markersTreeViewer.ts b/src/vs/workbench/parts/markers/electron-browser/markersTreeViewer.ts index bc55f39e66a..55577952d80 100644 --- a/src/vs/workbench/parts/markers/electron-browser/markersTreeViewer.ts +++ b/src/vs/workbench/parts/markers/electron-browser/markersTreeViewer.ts @@ -205,12 +205,13 @@ export class Renderer implements IRenderer { private renderMarkerElement(tree: ITree, element: Marker, templateData: IMarkerTemplateData) { let marker = element.raw; templateData.icon.className = 'icon ' + Renderer.iconClassNameFor(marker); + + templateData.source.set(marker.source, element.sourceMatches); + dom.toggleClass(templateData.source.element, 'marker-source', !!marker.source); + templateData.description.set(marker.message, element.messageMatches); templateData.description.element.title = marker.message; - dom.toggleClass(templateData.source.element, 'marker-source', !!marker.source); - templateData.source.set(marker.source, element.sourceMatches); - templateData.lnCol.textContent = Messages.MARKERS_PANEL_AT_LINE_COL_NUMBER(marker.startLineNumber, marker.startColumn); } diff --git a/src/vs/workbench/parts/markers/electron-browser/media/excludeSettings-dark.svg b/src/vs/workbench/parts/markers/electron-browser/media/excludeSettings-dark.svg new file mode 100755 index 00000000000..3eeedcb41c6 --- /dev/null +++ b/src/vs/workbench/parts/markers/electron-browser/media/excludeSettings-dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/workbench/parts/markers/electron-browser/media/excludeSettings.svg b/src/vs/workbench/parts/markers/electron-browser/media/excludeSettings.svg new file mode 100755 index 00000000000..79decb032b1 --- /dev/null +++ b/src/vs/workbench/parts/markers/electron-browser/media/excludeSettings.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/workbench/parts/markers/electron-browser/media/markers.css b/src/vs/workbench/parts/markers/electron-browser/media/markers.css index ace241621a5..27d64078e01 100644 --- a/src/vs/workbench/parts/markers/electron-browser/media/markers.css +++ b/src/vs/workbench/parts/markers/electron-browser/media/markers.css @@ -4,11 +4,18 @@ *--------------------------------------------------------------------------------------------*/ .monaco-action-bar .action-item.markers-panel-action-filter { - max-width: 400px; - min-width: 150px; - flex: 1; cursor: default; margin: 4px 10px 0 0; + min-width: 150px; + max-width: 500px; +} + +.monaco-action-bar .action-item.markers-panel-action-filter { + flex: 0.7; +} + +.monaco-action-bar .action-item.markers-panel-action-filter.small { + flex: 0.5; } .monaco-action-bar .action-item.markers-panel-action-filter .monaco-inputbox { @@ -20,6 +27,37 @@ border: 1px solid #ddd; } +.markers-panel-action-filter > .markers-panel-filter-controls { + position: absolute; + top: 0px; + right: 4px; + display: flex; +} + +.markers-panel-action-filter > .markers-panel-filter-controls > .markers-panel-filter-badge { + margin: 4px 0px; + padding: 0px 8px; + border-radius: 2px; +} + +.markers-panel-action-filter > .markers-panel-filter-controls > .markers-panel-filter-badge.hidden, +.markers-panel-action-filter > .markers-panel-filter-controls > .markers-panel-filter-badge.small { + display: none; +} + +.markers-panel-action-filter > .markers-panel-filter-controls > .markers-panel-filter-filesExclude { + margin: 3px 0 0 3px; +} + +.vs .markers-panel-action-filter > .markers-panel-filter-controls > .markers-panel-filter-filesExclude { + background: url('excludeSettings.svg') center center no-repeat; +} + +.vs-dark .markers-panel-action-filter > .markers-panel-filter-controls > .markers-panel-filter-filesExclude, +.hc-black .markers-panel-action-filter > .markers-panel-filter-controls > .markers-panel-filter-filesExclude { + background: url('excludeSettings-dark.svg') center center no-repeat; +} + .markers-panel .markers-panel-container { height: 100%; } @@ -29,18 +67,16 @@ padding-left: 20px; } -.markers-panel .markers-panel-container .message-box-container span:focus { - outline: none; +.markers-panel .markers-panel-container .message-box-container .messageAction { + margin-left: 4px; + cursor: pointer; + text-decoration: underline; } .markers-panel .markers-panel-container .hidden { display: none; } -.markers-panel .markers-panel-container .tree-container { - height: 100%; -} - .markers-panel .markers-panel-container .tree-container.hidden, .markers-panel .markers-panel-container .message-box-container.hidden { display: none; @@ -71,6 +107,10 @@ overflow: hidden; } +.markers-panel .markers-panel-container .tree-container .markers-panel-tree-entry .marker-source { + margin-right: 5px; +} + .markers-panel .markers-panel-container .tree-container .markers-panel-tree-entry .marker-source:before { content: '['; } @@ -118,4 +158,4 @@ .vs-dark .markers-panel .icon.info { background: url('status-info-inverse.svg') center center no-repeat; -} +} \ No newline at end of file diff --git a/src/vs/workbench/parts/markers/electron-browser/messages.ts b/src/vs/workbench/parts/markers/electron-browser/messages.ts index 03bc507f5d7..7368df79ae4 100644 --- a/src/vs/workbench/parts/markers/electron-browser/messages.ts +++ b/src/vs/workbench/parts/markers/electron-browser/messages.ts @@ -22,10 +22,14 @@ export default class Messages { public static MARKERS_PANEL_ARIA_LABEL_PROBLEMS_TREE: string = nls.localize('markers.panel.aria.label.problems.tree', "Problems grouped by files"); public static MARKERS_PANEL_NO_PROBLEMS_BUILT: string = nls.localize('markers.panel.no.problems.build', "No problems have been detected in the workspace so far."); - public static MARKERS_PANEL_NO_PROBLEMS_FILTERS: string = nls.localize('markers.panel.no.problems.filters', "No results found with provided filter criteria"); + public static MARKERS_PANEL_NO_PROBLEMS_FILTERS: string = nls.localize('markers.panel.no.problems.filters', "No results found with provided filter criteria."); + public static MARKERS_PANEL_NO_PROBLEMS_FILE_EXCLUSIONS_FILTER: string = nls.localize('markers.panel.no.problems.file.exclusions', "All problems are hidden because files exclude filter is enabled."); + public static MARKERS_PANEL_ACTION_TOOLTIP_USE_FILES_EXCLUDE: string = nls.localize('markers.panel.action.useFilesExclude', "Filter using Files Exclude Setting"); + public static MARKERS_PANEL_ACTION_TOOLTIP_DO_NOT_USE_FILES_EXCLUDE: string = nls.localize('markers.panel.action.donotUseFilesExclude', "Do not use Files Exclude Setting"); public static MARKERS_PANEL_ACTION_TOOLTIP_FILTER: string = nls.localize('markers.panel.action.filter', "Filter Problems"); - public static MARKERS_PANEL_FILTER_PLACEHOLDER: string = nls.localize('markers.panel.filter.placeholder', "Filter by type or text"); + public static MARKERS_PANEL_FILTER_ARIA_LABEL: string = nls.localize('markers.panel.filter.ariaLabel', "Filter Problems"); + public static MARKERS_PANEL_FILTER_PLACEHOLDER: string = nls.localize('markers.panel.filter.placeholder', "Filter. Eg: text, **/*.ts, !**/node_modules/**"); public static MARKERS_PANEL_FILTER_ERRORS: string = nls.localize('markers.panel.filter.errors', "errors"); public static MARKERS_PANEL_FILTER_WARNINGS: string = nls.localize('markers.panel.filter.warnings', "warnings"); public static MARKERS_PANEL_FILTER_INFOS: string = nls.localize('markers.panel.filter.infos', "infos"); diff --git a/src/vs/workbench/parts/markers/test/electron-browser/markersModel.test.ts b/src/vs/workbench/parts/markers/test/electron-browser/markersModel.test.ts index 043b5f9c536..9e2176bb7b6 100644 --- a/src/vs/workbench/parts/markers/test/electron-browser/markersModel.test.ts +++ b/src/vs/workbench/parts/markers/test/electron-browser/markersModel.test.ts @@ -7,8 +7,8 @@ import * as assert from 'assert'; import URI from 'vs/base/common/uri'; -import { IMarker, MarkerSeverity } from 'vs/platform/markers/common/markers'; -import { MarkersModel, Marker, ResourceMarkers } from 'vs/workbench/parts/markers/electron-browser/markersModel'; +import { IMarker, MarkerSeverity, IRelatedInformation } from 'vs/platform/markers/common/markers'; +import { MarkersModel, Marker, ResourceMarkers, RelatedInformation } from 'vs/workbench/parts/markers/electron-browser/markersModel'; class TestMarkersModel extends MarkersModel { @@ -139,12 +139,23 @@ suite('MarkersModel Test', () => { }); test('toString()', function () { - const res1Marker = aMarker('a/res1'); - res1Marker.code = '1234'; - assert.equal(`file: 'file:///a/res1'\nseverity: 'Error'\nmessage: 'some message'\nat: '10,5'\nsource: 'tslint'\ncode: '1234'`, new Marker('', res1Marker).toString()); - assert.equal(`file: 'file:///a/res2'\nseverity: 'Warning'\nmessage: 'some message'\nat: '10,5'\nsource: 'tslint'\ncode: ''`, new Marker('', aMarker('a/res2', MarkerSeverity.Warning)).toString()); - assert.equal(`file: 'file:///a/res2'\nseverity: 'Info'\nmessage: 'Info'\nat: '1,2'\nsource: ''\ncode: ''`, new Marker('', aMarker('a/res2', MarkerSeverity.Info, 1, 2, 1, 8, 'Info', '')).toString()); - assert.equal(`file: 'file:///a/res2'\nseverity: ''\nmessage: 'Ignore message'\nat: '1,2'\nsource: 'Ignore'\ncode: ''`, new Marker('', aMarker('a/res2', MarkerSeverity.Hint, 1, 2, 1, 8, 'Ignore message', 'Ignore')).toString()); + let marker = aMarker('a/res1'); + marker.code = '1234'; + assert.equal(JSON.stringify({ ...marker, resource: marker.resource.path }, null, '\t'), new Marker('', marker).toString()); + + marker = aMarker('a/res2', MarkerSeverity.Warning); + assert.equal(JSON.stringify({ ...marker, resource: marker.resource.path }, null, '\t'), new Marker('', marker).toString()); + + marker = aMarker('a/res2', MarkerSeverity.Info, 1, 2, 1, 8, 'Info', ''); + assert.equal(JSON.stringify({ ...marker, resource: marker.resource.path }, null, '\t'), new Marker('', marker).toString()); + + marker = aMarker('a/res2', MarkerSeverity.Hint, 1, 2, 1, 8, 'Ignore message', 'Ignore'); + assert.equal(JSON.stringify({ ...marker, resource: marker.resource.path }, null, '\t'), new Marker('', marker).toString()); + + marker = aMarker('a/res2', MarkerSeverity.Warning, 1, 2, 1, 8, 'Warning message', '', [{ startLineNumber: 2, startColumn: 5, endLineNumber: 2, endColumn: 10, message: 'some info', resource: URI.file('a/res3') }]); + const testObject = new Marker('', marker); + testObject.resourceRelatedInformation = marker.relatedInformation.map(r => new RelatedInformation('', r)); + assert.equal(JSON.stringify({ ...marker, resource: marker.resource.path, relatedInformation: marker.relatedInformation.map(r => ({ ...r, resource: r.resource.path })) }, null, '\t'), testObject.toString()); }); function hasMarker(markers: Marker[], marker: IMarker): boolean { @@ -200,7 +211,8 @@ suite('MarkersModel Test', () => { endLineNumber: number = startLineNumber + 1, endColumn: number = startColumn + 5, message: string = 'some message', - source: string = 'tslint' + source: string = 'tslint', + relatedInformation?: IRelatedInformation[] ): IMarker { return { owner: 'someOwner', @@ -211,7 +223,8 @@ suite('MarkersModel Test', () => { startColumn, endLineNumber, endColumn, - source + source, + relatedInformation }; } }); diff --git a/src/vs/workbench/parts/output/browser/outputActions.ts b/src/vs/workbench/parts/output/browser/outputActions.ts index ef5827ccc68..c73e611e656 100644 --- a/src/vs/workbench/parts/output/browser/outputActions.ts +++ b/src/vs/workbench/parts/output/browser/outputActions.ts @@ -42,15 +42,13 @@ export class ClearOutputAction extends Action { constructor( id: string, label: string, - @IOutputService private outputService: IOutputService, - @IPanelService private panelService: IPanelService + @IOutputService private outputService: IOutputService ) { super(id, label, 'output-action clear-output'); } public run(): TPromise { this.outputService.getActiveChannel().clear(); - this.panelService.getActivePanel().focus(); return TPromise.as(true); } diff --git a/src/vs/workbench/parts/output/browser/outputPanel.ts b/src/vs/workbench/parts/output/browser/outputPanel.ts index 36673ad91e0..35afc6d3e48 100644 --- a/src/vs/workbench/parts/output/browser/outputPanel.ts +++ b/src/vs/workbench/parts/output/browser/outputPanel.ts @@ -7,7 +7,6 @@ import 'vs/css!./media/output'; import * as nls from 'vs/nls'; import { TPromise } from 'vs/base/common/winjs.base'; import { Action, IAction } from 'vs/base/common/actions'; -import { Builder } from 'vs/base/browser/builder'; import { IActionItem } from 'vs/base/browser/ui/actionbar/actionbar'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -121,9 +120,9 @@ export class OutputPanel extends AbstractTextResourceEditor { super.clearInput(); } - protected createEditor(parent: Builder): void { + protected createEditor(parent: HTMLElement): void { // First create the scoped instantation service and only then construct the editor using the scoped service - const scopedContextKeyService = this.contextKeyService.createScoped(parent.getHTMLElement()); + const scopedContextKeyService = this.contextKeyService.createScoped(parent); this.toUnbind.push(scopedContextKeyService); this.scopedInstantiationService = this.instantiationService.createChild(new ServiceCollection([IContextKeyService, scopedContextKeyService])); super.createEditor(parent); diff --git a/src/vs/workbench/parts/output/electron-browser/outputServices.ts b/src/vs/workbench/parts/output/electron-browser/outputServices.ts index 9fd70a0a2f6..aac872844cb 100644 --- a/src/vs/workbench/parts/output/electron-browser/outputServices.ts +++ b/src/vs/workbench/parts/output/electron-browser/outputServices.ts @@ -5,7 +5,6 @@ import * as nls from 'vs/nls'; import * as paths from 'vs/base/common/paths'; -import * as strings from 'vs/base/common/strings'; import * as extfs from 'vs/base/node/extfs'; import * as fs from 'fs'; import { TPromise } from 'vs/base/common/winjs.base'; @@ -16,7 +15,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { Registry } from 'vs/platform/registry/common/platform'; import { EditorOptions } from 'vs/workbench/common/editor'; -import { IOutputChannelIdentifier, IOutputChannel, IOutputService, Extensions, OUTPUT_PANEL_ID, IOutputChannelRegistry, OUTPUT_SCHEME, OUTPUT_MIME, MAX_OUTPUT_LENGTH, LOG_SCHEME, LOG_MIME, CONTEXT_ACTIVE_LOG_OUTPUT } from 'vs/workbench/parts/output/common/output'; +import { IOutputChannelIdentifier, IOutputChannel, IOutputService, Extensions, OUTPUT_PANEL_ID, IOutputChannelRegistry, OUTPUT_SCHEME, OUTPUT_MIME, LOG_SCHEME, LOG_MIME, CONTEXT_ACTIVE_LOG_OUTPUT } from 'vs/workbench/parts/output/common/output'; import { OutputPanel } from 'vs/workbench/parts/output/browser/outputPanel'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; import { IModelService } from 'vs/editor/common/services/modelService'; @@ -36,8 +35,6 @@ import { RotatingLogger } from 'spdlog'; import { toLocalISOString } from 'vs/base/common/date'; import { IWindowService } from 'vs/platform/windows/common/windows'; import { ILogService } from 'vs/platform/log/common/log'; -import { binarySearch } from 'vs/base/common/arrays'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { Schemas } from 'vs/base/common/network'; import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; @@ -437,7 +434,6 @@ export class OutputService extends Disposable implements IOutputService, ITextMo @ITextModelService textModelResolverService: ITextModelService, @IEnvironmentService environmentService: IEnvironmentService, @IWindowService windowService: IWindowService, - @ITelemetryService private telemetryService: ITelemetryService, @ILogService private logService: ILogService, @ILifecycleService private lifecycleService: ILifecycleService, @IContextKeyService private contextKeyService: IContextKeyService, @@ -472,7 +468,7 @@ export class OutputService extends Disposable implements IOutputService, ITextMo } provideTextContent(resource: URI): TPromise { - const channel = this.getChannel(resource.fsPath); + const channel = this.getChannel(resource.path); if (channel) { return channel.loadModel(); } @@ -570,16 +566,9 @@ export class OutputService extends Disposable implements IOutputService, ITextMo } const uri = URI.from({ scheme: OUTPUT_SCHEME, path: id }); - if (channelData && channelData.file) { - return this.instantiationService.createInstance(FileOutputChannel, channelData, uri); - } - try { - return this.instantiationService.createInstance(OutputChannelBackedByFile, { id, label: channelData ? channelData.label : '' }, this.outputDir, uri); - } catch (e) { - this.logService.error(e); - this.telemetryService.publicLog('output.used.bufferedChannel'); - return this.instantiationService.createInstance(BufferredOutputChannel, { id, label: channelData ? channelData.label : '' }); - } + return channelData && channelData.file + ? this.instantiationService.createInstance(FileOutputChannel, channelData, uri) + : this.instantiationService.createInstance(OutputChannelBackedByFile, { id, label: channelData ? channelData.label : '' }, this.outputDir, uri); } private doShowChannel(channel: IOutputChannel, preserveFocus: boolean): TPromise { @@ -647,145 +636,4 @@ export class LogContentProvider { } return channel; } -} - -// Remove this channel when there are no issues using Output channel backed by file -class BufferredOutputChannel extends Disposable implements OutputChannel { - - readonly id: string; - readonly label: string; - readonly file: URI = null; - scrollLock: boolean = false; - - protected _onDidAppendedContent: Emitter = new Emitter(); - readonly onDidAppendedContent: Event = this._onDidAppendedContent.event; - - private readonly _onDispose: Emitter = new Emitter(); - readonly onDispose: Event = this._onDispose.event; - - private modelUpdater: RunOnceScheduler; - private model: ITextModel; - private readonly bufferredContent: BufferedContent; - private lastReadId: number = void 0; - - constructor( - protected readonly outputChannelIdentifier: IOutputChannelIdentifier, - @IModelService private modelService: IModelService, - @IModeService private modeService: IModeService - ) { - super(); - - this.id = outputChannelIdentifier.id; - this.label = outputChannelIdentifier.label; - - this.modelUpdater = new RunOnceScheduler(() => this.updateModel(), 300); - this._register(toDisposable(() => this.modelUpdater.cancel())); - - this.bufferredContent = new BufferedContent(); - this._register(toDisposable(() => this.bufferredContent.clear())); - } - - append(output: string) { - this.bufferredContent.append(output); - if (!this.modelUpdater.isScheduled()) { - this.modelUpdater.schedule(); - } - } - - clear(): void { - if (this.modelUpdater.isScheduled()) { - this.modelUpdater.cancel(); - } - if (this.model) { - this.model.setValue(''); - } - this.bufferredContent.clear(); - this.lastReadId = void 0; - } - - loadModel(): TPromise { - const { value, id } = this.bufferredContent.getDelta(this.lastReadId); - if (this.model) { - this.model.setValue(value); - } else { - this.model = this.createModel(value); - } - this.lastReadId = id; - return TPromise.as(this.model); - } - - private createModel(content: string): ITextModel { - const model = this.modelService.createModel(content, this.modeService.getOrCreateMode(OUTPUT_MIME), URI.from({ scheme: OUTPUT_SCHEME, path: this.id })); - const disposables: IDisposable[] = []; - disposables.push(model.onWillDispose(() => { - this.model = null; - dispose(disposables); - })); - return model; - } - - private updateModel(): void { - if (this.model) { - const { value, id } = this.bufferredContent.getDelta(this.lastReadId); - this.lastReadId = id; - const lastLine = this.model.getLineCount(); - const lastLineMaxColumn = this.model.getLineMaxColumn(lastLine); - this.model.applyEdits([EditOperation.insert(new Position(lastLine, lastLineMaxColumn), value)]); - this._onDidAppendedContent.fire(); - } - } - - dispose(): void { - this._onDispose.fire(); - super.dispose(); - } -} - -class BufferedContent { - - private data: string[] = []; - private dataIds: number[] = []; - private idPool = 0; - private length = 0; - - public append(content: string): void { - this.data.push(content); - this.dataIds.push(++this.idPool); - this.length += content.length; - this.trim(); - } - - public clear(): void { - this.data.length = 0; - this.dataIds.length = 0; - this.length = 0; - } - - private trim(): void { - if (this.length < MAX_OUTPUT_LENGTH * 1.2) { - return; - } - - while (this.length > MAX_OUTPUT_LENGTH) { - this.dataIds.shift(); - const removed = this.data.shift(); - this.length -= removed.length; - } - } - - public getDelta(previousId?: number): { value: string, id: number } { - let idx = -1; - if (previousId !== void 0) { - idx = binarySearch(this.dataIds, previousId, (a, b) => a - b); - } - - const id = this.idPool; - if (idx >= 0) { - const value = strings.removeAnsiEscapeCodes(this.data.slice(idx + 1).join('')); - return { value, id }; - } else { - const value = strings.removeAnsiEscapeCodes(this.data.join('')); - return { value, id }; - } - } -} +} \ No newline at end of file diff --git a/src/vs/workbench/parts/preferences/browser/keybindingWidgets.ts b/src/vs/workbench/parts/preferences/browser/keybindingWidgets.ts index 788fd9dbc1d..237f4d4efa9 100644 --- a/src/vs/workbench/parts/preferences/browser/keybindingWidgets.ts +++ b/src/vs/workbench/parts/preferences/browser/keybindingWidgets.ts @@ -17,7 +17,6 @@ import { InputBox, IInputOptions } from 'vs/base/browser/ui/inputbox/inputBox'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { Dimension } from 'vs/base/browser/builder'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition } from 'vs/editor/browser/editorBrowser'; @@ -189,7 +188,7 @@ export class DefineKeybindingWidget extends Widget { }); } - layout(layout: Dimension): void { + layout(layout: dom.Dimension): void { let top = Math.round((layout.height - DefineKeybindingWidget.HEIGHT) / 2); this._domNode.setTop(top); @@ -290,7 +289,7 @@ export class DefineKeybindingOverlayWidget extends Disposable implements IOverla public start(): TPromise { this._editor.revealPositionInCenterIfOutsideViewport(this._editor.getPosition(), ScrollType.Smooth); const layoutInfo = this._editor.getLayoutInfo(); - this._widget.layout(new Dimension(layoutInfo.width, layoutInfo.height)); + this._widget.layout(new dom.Dimension(layoutInfo.width, layoutInfo.height)); return this._widget.define(); } } diff --git a/src/vs/workbench/parts/preferences/browser/keybindingsEditor.ts b/src/vs/workbench/parts/preferences/browser/keybindingsEditor.ts index a19ef06edf6..9511b94d628 100644 --- a/src/vs/workbench/parts/preferences/browser/keybindingsEditor.ts +++ b/src/vs/workbench/parts/preferences/browser/keybindingsEditor.ts @@ -11,22 +11,21 @@ import * as DOM from 'vs/base/browser/dom'; import { OS } from 'vs/base/common/platform'; import { dispose } from 'vs/base/common/lifecycle'; import { Checkbox } from 'vs/base/browser/ui/checkbox/checkbox'; -import { Builder, Dimension } from 'vs/base/browser/builder'; import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; import { KeybindingLabel } from 'vs/base/browser/ui/keybindingLabel/keybindingLabel'; import { IAction } from 'vs/base/common/actions'; import { ActionBar, Separator } from 'vs/base/browser/ui/actionbar/actionbar'; import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; -import { EditorInput, EditorOptions } from 'vs/workbench/common/editor'; +import { EditorOptions } from 'vs/workbench/common/editor'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; -import { KeybindingsEditorModel, IKeybindingItemEntry, IListEntry, KEYBINDING_ENTRY_TEMPLATE_ID, KEYBINDING_HEADER_TEMPLATE_ID } from 'vs/workbench/parts/preferences/common/keybindingsEditorModel'; +import { KeybindingsEditorModel, IKeybindingItemEntry, IListEntry, KEYBINDING_ENTRY_TEMPLATE_ID, KEYBINDING_HEADER_TEMPLATE_ID } from 'vs/workbench/services/preferences/common/keybindingsEditorModel'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService, IUserFriendlyKeybinding } from 'vs/platform/keybinding/common/keybinding'; import { SearchWidget } from 'vs/workbench/parts/preferences/browser/preferencesWidgets'; import { DefineKeybindingWidget } from 'vs/workbench/parts/preferences/browser/keybindingWidgets'; import { - IPreferencesService, IKeybindingsEditor, CONTEXT_KEYBINDING_FOCUS, CONTEXT_KEYBINDINGS_EDITOR, CONTEXT_KEYBINDINGS_SEARCH_FOCUS, KEYBINDINGS_EDITOR_COMMAND_REMOVE, KEYBINDINGS_EDITOR_COMMAND_COPY, + IKeybindingsEditor, CONTEXT_KEYBINDING_FOCUS, CONTEXT_KEYBINDINGS_EDITOR, CONTEXT_KEYBINDINGS_SEARCH_FOCUS, KEYBINDINGS_EDITOR_COMMAND_REMOVE, KEYBINDINGS_EDITOR_COMMAND_COPY, KEYBINDINGS_EDITOR_COMMAND_RESET, KEYBINDINGS_EDITOR_COMMAND_COPY_COMMAND, KEYBINDINGS_EDITOR_COMMAND_DEFINE, KEYBINDINGS_EDITOR_COMMAND_SHOW_SIMILAR, KEYBINDINGS_EDITOR_SHOW_DEFAULT_KEYBINDINGS, KEYBINDINGS_EDITOR_SHOW_USER_KEYBINDINGS } from 'vs/workbench/parts/preferences/common/preferences'; @@ -43,36 +42,11 @@ import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/edi import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions'; import { WorkbenchList } from 'vs/platform/list/browser/listService'; import { INotificationService } from 'vs/platform/notification/common/notification'; +import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; +import { KeybindingsEditorInput } from 'vs/workbench/services/preferences/common/preferencesEditorInput'; let $ = DOM.$; -export class KeybindingsEditorInput extends EditorInput { - - public static readonly ID: string = 'workbench.input.keybindings'; - public readonly keybindingsModel: KeybindingsEditorModel; - - constructor(@IInstantiationService instantiationService: IInstantiationService) { - super(); - this.keybindingsModel = instantiationService.createInstance(KeybindingsEditorModel, OS); - } - - getTypeId(): string { - return KeybindingsEditorInput.ID; - } - - getName(): string { - return localize('keybindingsInputName', "Keyboard Shortcuts"); - } - - resolve(refresh?: boolean): TPromise { - return TPromise.as(this.keybindingsModel); - } - - matches(otherInput: any): boolean { - return otherInput instanceof KeybindingsEditorInput; - } -} - export class KeybindingsEditor extends BaseEditor implements IKeybindingsEditor { public static readonly ID: string = 'workbench.editor.keybindings'; @@ -90,7 +64,7 @@ export class KeybindingsEditor extends BaseEditor implements IKeybindingsEditor private listEntries: IListEntry[]; private keybindingsList: List; - private dimension: Dimension; + private dimension: DOM.Dimension; private delayedFiltering: Delayer; private latestEmptyFilters: string[] = []; private delayedFilterLogging: Delayer; @@ -122,16 +96,14 @@ export class KeybindingsEditor extends BaseEditor implements IKeybindingsEditor this.delayedFilterLogging = new Delayer(1000); } - createEditor(parent: Builder): void { - const parentElement = parent.getHTMLElement(); - - const keybindingsEditorElement = DOM.append(parentElement, $('div', { class: 'keybindings-editor' })); + createEditor(parent: HTMLElement): void { + const keybindingsEditorElement = DOM.append(parent, $('div', { class: 'keybindings-editor' })); this.createOverlayContainer(keybindingsEditorElement); this.createHeader(keybindingsEditorElement); this.createBody(keybindingsEditorElement); - const focusTracker = this._register(DOM.trackFocus(parentElement)); + const focusTracker = this._register(DOM.trackFocus(parent)); this._register(focusTracker.onDidFocus(() => this.keybindingsEditorContextKey.set(true))); this._register(focusTracker.onDidBlur(() => this.keybindingsEditorContextKey.reset())); } @@ -152,7 +124,7 @@ export class KeybindingsEditor extends BaseEditor implements IKeybindingsEditor this.keybindingFocusContextKey.reset(); } - layout(dimension: Dimension): void { + layout(dimension: DOM.Dimension): void { this.dimension = dimension; this.searchWidget.layout(dimension); diff --git a/src/vs/workbench/parts/preferences/browser/preferencesActions.ts b/src/vs/workbench/parts/preferences/browser/preferencesActions.ts index 0399b803ed0..da2948671a3 100644 --- a/src/vs/workbench/parts/preferences/browser/preferencesActions.ts +++ b/src/vs/workbench/parts/preferences/browser/preferencesActions.ts @@ -11,7 +11,7 @@ import { Action } from 'vs/base/common/actions'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { IModeService } from 'vs/editor/common/services/modeService'; import { IQuickOpenService, IPickOpenEntry, IFilePickOpenEntry } from 'vs/platform/quickOpen/common/quickOpen'; -import { IPreferencesService } from 'vs/workbench/parts/preferences/common/preferences'; +import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; import { IWorkspaceContextService, WorkbenchState, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { PICK_WORKSPACE_FOLDER_COMMAND_ID } from 'vs/workbench/browser/actions/workspaceCommands'; @@ -34,6 +34,24 @@ export class OpenRawDefaultSettingsAction extends Action { } } +export class OpenSettingsAction extends Action { + + public static readonly ID = 'workbench.action.openSettings'; + public static readonly LABEL = nls.localize('openSettings', "Open Settings"); + + constructor( + id: string, + label: string, + @IPreferencesService private preferencesService: IPreferencesService + ) { + super(id, label); + } + + public run(event?: any): TPromise { + return this.preferencesService.openSettings(); + } +} + export class OpenGlobalSettingsAction extends Action { public static readonly ID = 'workbench.action.openGlobalSettings'; diff --git a/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts b/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts index 266ea2a1400..7e9a04f3ab4 100644 --- a/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts +++ b/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts @@ -11,24 +11,25 @@ import { onUnexpectedError, isPromiseCanceledError, getErrorMessage } from 'vs/b import * as DOM from 'vs/base/browser/dom'; import { Delayer, ThrottledDelayer } from 'vs/base/common/async'; import * as arrays from 'vs/base/common/arrays'; -import { Dimension, Builder } from 'vs/base/browser/builder'; import { ArrayNavigator } from 'vs/base/common/iterator'; import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; -import { SideBySideEditorInput, EditorOptions, EditorInput, PREFERENCES_EDITOR_ID } from 'vs/workbench/common/editor'; +import { EditorOptions, EditorInput } from 'vs/workbench/common/editor'; import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; import { ResourceEditorModel } from 'vs/workbench/common/editor/resourceEditorModel'; -import { IEditorControl, Position, Verbosity } from 'vs/platform/editor/common/editor'; -import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; +import { IEditorControl, Position } from 'vs/platform/editor/common/editor'; import * as editorCommon from 'vs/editor/common/editorCommon'; import { BaseTextEditor } from 'vs/workbench/browser/parts/editor/textEditor'; import { CodeEditor } from 'vs/editor/browser/codeEditor'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { - IPreferencesService, ISettingsGroup, ISetting, IFilterResult, IPreferencesSearchService, - CONTEXT_SETTINGS_EDITOR, CONTEXT_SETTINGS_SEARCH_FOCUS, SETTINGS_EDITOR_COMMAND_SEARCH, SETTINGS_EDITOR_COMMAND_FOCUS_FILE, ISettingsEditorModel, SETTINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS, SETTINGS_EDITOR_COMMAND_FOCUS_NEXT_SETTING, SETTINGS_EDITOR_COMMAND_FOCUS_PREVIOUS_SETTING, ISearchProvider, ISearchResult, SETTINGS_EDITOR_COMMAND_EDIT_FOCUSED_SETTING + IPreferencesSearchService, + CONTEXT_SETTINGS_EDITOR, CONTEXT_SETTINGS_SEARCH_FOCUS, SETTINGS_EDITOR_COMMAND_SEARCH, SETTINGS_EDITOR_COMMAND_FOCUS_FILE, SETTINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS, SETTINGS_EDITOR_COMMAND_FOCUS_NEXT_SETTING, SETTINGS_EDITOR_COMMAND_FOCUS_PREVIOUS_SETTING, ISearchProvider, SETTINGS_EDITOR_COMMAND_EDIT_FOCUSED_SETTING } from 'vs/workbench/parts/preferences/common/preferences'; -import { SettingsEditorModel, DefaultSettingsEditorModel } from 'vs/workbench/parts/preferences/common/preferencesModels'; +import { + IPreferencesService, ISettingsGroup, ISetting, IFilterResult, ISettingsEditorModel, ISearchResult +} from 'vs/workbench/services/preferences/common/preferences'; +import { SettingsEditorModel, DefaultSettingsEditorModel } from 'vs/workbench/services/preferences/common/preferencesModels'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { SearchWidget, SettingsTargetsWidget, SettingsTarget } from 'vs/workbench/parts/preferences/browser/preferencesWidgets'; import { ContextKeyExpr, IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; @@ -38,7 +39,6 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/resourceConfiguration'; import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { VSash } from 'vs/base/browser/ui/sash/sash'; import { Widget } from 'vs/base/browser/ui/widget'; import { IPreferencesRenderer, DefaultSettingsRenderer, UserSettingsRenderer, WorkspaceSettingsRenderer, FolderSettingsRenderer } from 'vs/workbench/parts/preferences/browser/preferencesRenderers'; @@ -57,51 +57,11 @@ import { Event, Emitter } from 'vs/base/common/event'; import { Registry } from 'vs/platform/registry/common/platform'; import { MessageController } from 'vs/editor/contrib/message/messageController'; import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; -import { IHashService } from 'vs/workbench/services/hash/common/hashService'; -import { ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; import { IStringDictionary } from 'vs/base/common/collections'; import { IProgressService } from 'vs/platform/progress/common/progress'; import { ILogService } from 'vs/platform/log/common/log'; - -export class PreferencesEditorInput extends SideBySideEditorInput { - public static readonly ID: string = 'workbench.editorinputs.preferencesEditorInput'; - - getTypeId(): string { - return PreferencesEditorInput.ID; - } - - public supportsSplitEditor(): boolean { - return true; - } - - public getTitle(verbosity: Verbosity): string { - return this.master.getTitle(verbosity); - } -} - -export class DefaultPreferencesEditorInput extends ResourceEditorInput { - public static readonly ID = 'workbench.editorinputs.defaultpreferences'; - constructor(defaultSettingsResource: URI, - @ITextModelService textModelResolverService: ITextModelService, - @IHashService hashService: IHashService - ) { - super(nls.localize('settingsEditorName', "Default Settings"), '', defaultSettingsResource, textModelResolverService, hashService); - } - - getTypeId(): string { - return DefaultPreferencesEditorInput.ID; - } - - matches(other: any): boolean { - if (other instanceof DefaultPreferencesEditorInput) { - return true; - } - if (!super.matches(other)) { - return false; - } - return true; - } -} +import { PreferencesEditorInput, DefaultPreferencesEditorInput } from 'vs/workbench/services/preferences/common/preferencesEditorInput'; +import { PREFERENCES_EDITOR_ID } from 'vs/workbench/parts/files/common/files'; export class PreferencesEditor extends BaseEditor { @@ -138,11 +98,10 @@ export class PreferencesEditor extends BaseEditor { this.remoteSearchThrottle = new ThrottledDelayer(200); } - public createEditor(parent: Builder): void { - const parentElement = parent.getHTMLElement(); - DOM.addClass(parentElement, 'preferences-editor'); + public createEditor(parent: HTMLElement): void { + DOM.addClass(parent, 'preferences-editor'); - this.headerContainer = DOM.append(parentElement, DOM.$('.preferences-header')); + this.headerContainer = DOM.append(parent, DOM.$('.preferences-header')); this.searchWidget = this._register(this.instantiationService.createInstance(SearchWidget, this.headerContainer, { ariaLabel: nls.localize('SearchSettingsWidget.AriaLabel', "Search settings"), @@ -154,7 +113,7 @@ export class PreferencesEditor extends BaseEditor { this._register(this.searchWidget.onFocus(() => this.lastFocusedWidget = this.searchWidget)); this.lastFocusedWidget = this.searchWidget; - const editorsContainer = DOM.append(parentElement, DOM.$('.preferences-editors-container')); + const editorsContainer = DOM.append(parent, DOM.$('.preferences-editors-container')); this.sideBySidePreferencesWidget = this._register(this.instantiationService.createInstance(SideBySidePreferencesWidget, editorsContainer)); this._register(this.sideBySidePreferencesWidget.onFocus(() => this.lastFocusedWidget = this.sideBySidePreferencesWidget)); this._register(this.sideBySidePreferencesWidget.onDidSettingsTargetChange(target => this.switchSettings(target))); @@ -192,11 +151,11 @@ export class PreferencesEditor extends BaseEditor { return super.setInput(newInput, options).then(() => this.updateInput(oldInput, newInput, options)); } - public layout(dimension: Dimension): void { + public layout(dimension: DOM.Dimension): void { DOM.toggleClass(this.headerContainer, 'vertical-layout', dimension.width < 700); this.searchWidget.layout(dimension); const headerHeight = DOM.getTotalHeight(this.headerContainer); - this.sideBySidePreferencesWidget.layout(new Dimension(dimension.width, dimension.height - headerHeight)); + this.sideBySidePreferencesWidget.layout(new DOM.Dimension(dimension.width, dimension.height - headerHeight)); } public getControl(): IEditorControl { @@ -230,6 +189,10 @@ export class PreferencesEditor extends BaseEditor { super.clearInput(); } + public supportsCenteredLayout(): boolean { + return false; + } + protected setEditorVisible(visible: boolean, position: Position): void { this.sideBySidePreferencesWidget.setEditorVisible(visible, position); super.setEditorVisible(visible, position); @@ -772,7 +735,7 @@ class PreferencesRenderersController extends Disposable { class SideBySidePreferencesWidget extends Widget { - private dimension: Dimension; + private dimension: DOM.Dimension; private defaultPreferencesHeader: HTMLElement; private defaultPreferencesEditor: DefaultPreferencesEditor; @@ -817,7 +780,7 @@ class SideBySidePreferencesWidget extends Widget { this.defaultPreferencesHeader.textContent = nls.localize('defaultSettings', "Default Settings"); this.defaultPreferencesEditor = this._register(this.instantiationService.createInstance(DefaultPreferencesEditor)); - this.defaultPreferencesEditor.create(new Builder(this.defaultPreferencesEditorContainer)); + this.defaultPreferencesEditor.create(this.defaultPreferencesEditorContainer); this.defaultPreferencesEditor.setVisible(true); (this.defaultPreferencesEditor.getControl()).onDidFocusEditor(() => this.lastFocusedEditor = this.defaultPreferencesEditor); @@ -850,16 +813,28 @@ class SideBySidePreferencesWidget extends Widget { return TPromise.join([this.updateInput(this.defaultPreferencesEditor, defaultPreferencesEditorInput, DefaultSettingsEditorContribution.ID, editablePreferencesEditorInput.getResource(), options), this.updateInput(this.editablePreferencesEditor, editablePreferencesEditorInput, SettingsEditorContribution.ID, defaultPreferencesEditorInput.getResource(), options)]) .then(([defaultPreferencesRenderer, editablePreferencesRenderer]) => { - this.defaultPreferencesHeader.textContent = defaultPreferencesRenderer && (defaultPreferencesRenderer.preferencesModel).configurationScope === ConfigurationScope.RESOURCE ? nls.localize('defaultFolderSettings', "Default Folder Settings") : nls.localize('defaultSettings', "Default Settings"); + this.defaultPreferencesHeader.textContent = defaultPreferencesRenderer && this.getDefaultPreferencesHeaderText((defaultPreferencesRenderer.preferencesModel).target); return { defaultPreferencesRenderer, editablePreferencesRenderer }; }); } + private getDefaultPreferencesHeaderText(target: ConfigurationTarget): string { + switch (target) { + case ConfigurationTarget.USER: + return nls.localize('defaultUserSettings', "Default User Settings"); + case ConfigurationTarget.WORKSPACE: + return nls.localize('defaultWorkspaceSettings', "Default Workspace Settings"); + case ConfigurationTarget.WORKSPACE_FOLDER: + return nls.localize('defaultFolderSettings', "Default Folder Settings"); + } + return ''; + } + public setResultCount(settingsTarget: SettingsTarget, count: number): void { this.settingsTargetsWidget.setResultCount(settingsTarget, count); } - public layout(dimension: Dimension): void { + public layout(dimension: DOM.Dimension): void { this.dimension = dimension; this.sash.setDimenesion(this.dimension); } @@ -902,7 +877,7 @@ class SideBySidePreferencesWidget extends Widget { const descriptor = Registry.as(EditorExtensions.Editors).getEditor(editorInput); const editor = descriptor.instantiate(this.instantiationService); this.editablePreferencesEditor = editor; - this.editablePreferencesEditor.create(new Builder(this.editablePreferencesEditorContainer)); + this.editablePreferencesEditor.create(this.editablePreferencesEditorContainer); this.editablePreferencesEditor.setVisible(true); (this.editablePreferencesEditor.getControl()).onDidFocusEditor(() => this.lastFocusedEditor = this.editablePreferencesEditor); this.lastFocusedEditor = this.editablePreferencesEditor; @@ -935,8 +910,8 @@ class SideBySidePreferencesWidget extends Widget { this.editablePreferencesEditorContainer.style.height = `${this.dimension.height}px`; this.editablePreferencesEditorContainer.style.left = `${splitPoint}px`; - this.defaultPreferencesEditor.layout(new Dimension(detailsEditorWidth, this.dimension.height - 34 /* height of header container */)); - this.editablePreferencesEditor.layout(new Dimension(masterEditorWidth, this.dimension.height - 34 /* height of header container */)); + this.defaultPreferencesEditor.layout(new DOM.Dimension(detailsEditorWidth, this.dimension.height - 34 /* height of header container */)); + this.editablePreferencesEditor.layout(new DOM.Dimension(masterEditorWidth, this.dimension.height - 34 /* height of header container */)); } private getSettingsTarget(resource: URI): SettingsTarget { @@ -990,8 +965,8 @@ export class DefaultPreferencesEditor extends BaseTextEditor { super(DefaultPreferencesEditor.ID, telemetryService, instantiationService, storageService, configurationService, themeService, textFileService, editorGroupService); } - public createEditorControl(parent: Builder, configuration: IEditorOptions): editorCommon.IEditor { - const editor = this.instantiationService.createInstance(DefaultPreferencesCodeEditor, parent.getHTMLElement(), configuration); + public createEditorControl(parent: HTMLElement, configuration: IEditorOptions): editorCommon.IEditor { + const editor = this.instantiationService.createInstance(DefaultPreferencesCodeEditor, parent, configuration); // Inform user about editor being readonly if user starts type this.toUnbind.push(editor.onDidType(() => this.showReadonlyHint(editor))); @@ -1042,10 +1017,14 @@ export class DefaultPreferencesEditor extends BaseTextEditor { super.clearInput(); } - public layout(dimension: Dimension) { + public layout(dimension: DOM.Dimension) { this.getControl().layout(dimension); } + public supportsCenteredLayout(): boolean { + return false; + } + protected getAriaLabel(): string { return nls.localize('preferencesAriaLabel', "Default preferences. Readonly text editor."); } diff --git a/src/vs/workbench/parts/preferences/browser/preferencesRenderers.ts b/src/vs/workbench/parts/preferences/browser/preferencesRenderers.ts index 45f83466767..4a11f390a57 100644 --- a/src/vs/workbench/parts/preferences/browser/preferencesRenderers.ts +++ b/src/vs/workbench/parts/preferences/browser/preferencesRenderers.ts @@ -18,18 +18,16 @@ import * as editorCommon from 'vs/editor/common/editorCommon'; import { Range, IRange } from 'vs/editor/common/core/range'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope, IConfigurationPropertySchema } from 'vs/platform/configuration/common/configurationRegistry'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IPreferencesService, ISettingsGroup, ISetting, IPreferencesEditorModel, IFilterResult, ISettingsEditorModel, IWorkbenchSettingsConfiguration, IExtensionSetting, IScoredResults } from 'vs/workbench/parts/preferences/common/preferences'; -import { SettingsEditorModel, DefaultSettingsEditorModel, WorkspaceConfigurationEditorModel } from 'vs/workbench/parts/preferences/common/preferencesModels'; +import { IPreferencesService, ISettingsGroup, ISetting, IPreferencesEditorModel, IFilterResult, ISettingsEditorModel, IExtensionSetting, IScoredResults } from 'vs/workbench/services/preferences/common/preferences'; +import { SettingsEditorModel, DefaultSettingsEditorModel, WorkspaceConfigurationEditorModel } from 'vs/workbench/services/preferences/common/preferencesModels'; import { ICodeEditor, IEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { SettingsGroupTitleWidget, EditPreferenceWidget, SettingsHeaderWidget, DefaultSettingsHeaderWidget, FloatingClickWidget } from 'vs/workbench/parts/preferences/browser/preferencesWidgets'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { RangeHighlightDecorations } from 'vs/workbench/browser/parts/editor/rangeDecorations'; -import { IMarkerService, IMarkerData, MarkerSeverity } from 'vs/platform/markers/common/markers'; import { ICursorPositionChangedEvent } from 'vs/editor/common/controller/cursorEvents'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; -import { MarkdownString } from 'vs/base/common/htmlContent'; import { overrideIdentifierFromKey, IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { ITextModel, IModelDeltaDecoration, TrackedRangeStickiness } from 'vs/editor/common/model'; @@ -42,6 +40,7 @@ import { IWorkbenchIssueService } from 'vs/workbench/services/issue/common/issue import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { ContextSubMenu } from 'vs/base/browser/contextmenu'; +import { IWorkbenchSettingsConfiguration } from 'vs/workbench/parts/preferences/common/preferences'; export interface IPreferencesRenderer extends IDisposable { readonly preferencesModel: IPreferencesEditorModel; @@ -188,7 +187,6 @@ export class UserSettingsRenderer extends Disposable implements IPreferencesRend export class WorkspaceSettingsRenderer extends UserSettingsRenderer implements IPreferencesRenderer { - private unsupportedSettingsRenderer: UnsupportedSettingsRenderer; private workspaceConfigurationRenderer: WorkspaceConfigurationRenderer; constructor(editor: ICodeEditor, preferencesModel: SettingsEditorModel, @@ -198,7 +196,6 @@ export class WorkspaceSettingsRenderer extends UserSettingsRenderer implements I @IInstantiationService instantiationService: IInstantiationService ) { super(editor, preferencesModel, preferencesService, configurationService, instantiationService); - this.unsupportedSettingsRenderer = this._register(instantiationService.createInstance(UnsupportedSettingsRenderer, editor, preferencesModel)); this.workspaceConfigurationRenderer = this._register(instantiationService.createInstance(WorkspaceConfigurationRenderer, editor, preferencesModel)); } @@ -213,15 +210,12 @@ export class WorkspaceSettingsRenderer extends UserSettingsRenderer implements I public render(): void { super.render(); - this.unsupportedSettingsRenderer.render(); this.workspaceConfigurationRenderer.render(this.getAssociatedPreferencesModel()); } } export class FolderSettingsRenderer extends UserSettingsRenderer implements IPreferencesRenderer { - private unsupportedSettingsRenderer: UnsupportedSettingsRenderer; - constructor(editor: ICodeEditor, preferencesModel: SettingsEditorModel, @IPreferencesService preferencesService: IPreferencesService, @ITelemetryService telemetryService: ITelemetryService, @@ -229,17 +223,12 @@ export class FolderSettingsRenderer extends UserSettingsRenderer implements IPre @IInstantiationService instantiationService: IInstantiationService ) { super(editor, preferencesModel, preferencesService, configurationService, instantiationService); - this.unsupportedSettingsRenderer = this._register(instantiationService.createInstance(UnsupportedSettingsRenderer, editor, preferencesModel)); } protected createHeader(): void { this._register(new SettingsHeaderWidget(this.editor, '')).setMessage(nls.localize('emptyFolderSettingsHeader', "Place your folder settings here to overwrite those from the Workspace Settings.")); } - public render(): void { - super.render(); - this.unsupportedSettingsRenderer.render(); - } } export class DefaultSettingsRenderer extends Disposable implements IPreferencesRenderer { @@ -273,7 +262,7 @@ export class DefaultSettingsRenderer extends Disposable implements IPreferencesR ) { super(); this.settingHighlighter = this._register(instantiationService.createInstance(SettingHighlighter, editor, this._onFocusPreference, this._onClearFocusPreference)); - this.settingsHeaderRenderer = this._register(instantiationService.createInstance(DefaultSettingsHeaderRenderer, editor, preferencesModel.configurationScope)); + this.settingsHeaderRenderer = this._register(instantiationService.createInstance(DefaultSettingsHeaderRenderer, editor)); this.settingsGroupTitleRenderer = this._register(instantiationService.createInstance(SettingsGroupTitleRenderer, editor)); this.filteredMatchesRenderer = this._register(instantiationService.createInstance(FilteredMatchesRenderer, editor)); this.editSettingActionRenderer = this._register(instantiationService.createInstance(EditSettingRenderer, editor, preferencesModel, this.settingHighlighter)); @@ -467,7 +456,7 @@ class DefaultSettingsHeaderRenderer extends Disposable { private settingsHeaderWidget: DefaultSettingsHeaderWidget; public readonly onClick: Event; - constructor(editor: ICodeEditor, scope: ConfigurationScope) { + constructor(editor: ICodeEditor) { super(); this.settingsHeaderWidget = this._register(new DefaultSettingsHeaderWidget(editor, '')); this.onClick = this.settingsHeaderWidget.onClick; @@ -879,29 +868,27 @@ export class FilteredMatchesRenderer extends Disposable implements HiddenAreasPr public render(result: IFilterResult, allSettingsGroups: ISettingsGroup[]): void { const model = this.editor.getModel(); this.hiddenAreas = []; - this.editor.changeDecorations(changeAccessor => { - this.decorationIds = changeAccessor.deltaDecorations(this.decorationIds, []); - }); if (result) { this.hiddenAreas = this.computeHiddenRanges(result.filteredGroups, result.allGroups, model); - this.editor.changeDecorations(changeAccessor => { - this.decorationIds = changeAccessor.deltaDecorations(this.decorationIds, result.matches.map(match => this.createDecoration(match, model))); - }); + this.decorationIds = this.editor.deltaDecorations(this.decorationIds, result.matches.map(match => this.createDecoration(match, model))); } else { this.hiddenAreas = this.computeHiddenRanges(null, allSettingsGroups, model); + this.decorationIds = this.editor.deltaDecorations(this.decorationIds, []); } } private createDecoration(range: IRange, model: ITextModel): IModelDeltaDecoration { return { range, - options: { - stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, - className: 'findMatch' - } + options: FilteredMatchesRenderer._FIND_MATCH }; } + private static readonly _FIND_MATCH = ModelDecorationOptions.register({ + stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, + className: 'findMatch' + }); + private computeHiddenRanges(filteredGroups: ISettingsGroup[], allSettingsGroups: ISettingsGroup[], model: ITextModel): IRange[] { // Hide the contents of hidden groups const notMatchesRanges: IRange[] = []; @@ -920,11 +907,7 @@ export class FilteredMatchesRenderer extends Disposable implements HiddenAreasPr } public dispose() { - if (this.decorationIds) { - this.decorationIds = this.editor.changeDecorations(changeAccessor => { - return changeAccessor.deltaDecorations(this.decorationIds, []); - }); - } + this.decorationIds = this.editor.deltaDecorations(this.decorationIds, []); super.dispose(); } } @@ -940,14 +923,7 @@ export class HighlightMatchesRenderer extends Disposable { public render(matches: IRange[]): void { const model = this.editor.getModel(); - this.editor.changeDecorations(changeAccessor => { - this.decorationIds = changeAccessor.deltaDecorations(this.decorationIds, []) || []; - }); - if (matches.length) { - this.editor.changeDecorations(changeAccessor => { - this.decorationIds = changeAccessor.deltaDecorations(this.decorationIds, matches.map(match => this.createDecoration(match, model))) || []; - }); - } + this.decorationIds = this.editor.deltaDecorations(this.decorationIds, matches.map(match => this.createDecoration(match, model))); } private static readonly _FIND_MATCH = ModelDecorationOptions.register({ @@ -963,11 +939,7 @@ export class HighlightMatchesRenderer extends Disposable { } public dispose() { - if (this.decorationIds) { - this.decorationIds = this.editor.changeDecorations(changeAccessor => { - return changeAccessor.deltaDecorations(this.decorationIds, []); - }) || []; - } + this.decorationIds = this.editor.deltaDecorations(this.decorationIds, []); super.dispose(); } } @@ -1329,104 +1301,6 @@ class SettingHighlighter extends Disposable { } } -class UnsupportedSettingsRenderer extends Disposable { - - private decorationIds: string[] = []; - private renderingDelayer: Delayer = new Delayer(200); - - constructor( - private editor: ICodeEditor, - private settingsEditorModel: SettingsEditorModel, - @IMarkerService private markerService: IMarkerService, - @IEnvironmentService private environmentService: IEnvironmentService - ) { - super(); - this._register(this.editor.getModel().onDidChangeContent(() => this.renderingDelayer.trigger(() => this.render()))); - } - - public render(): void { - const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration).getConfigurationProperties(); - const ranges: IRange[] = []; - const markerData: IMarkerData[] = []; - for (const settingsGroup of this.settingsEditorModel.settingsGroups) { - for (const section of settingsGroup.sections) { - for (const setting of section.settings) { - if (this.settingsEditorModel.configurationTarget === ConfigurationTarget.WORKSPACE || this.settingsEditorModel.configurationTarget === ConfigurationTarget.WORKSPACE_FOLDER) { - // Show warnings for executable settings - if (configurationRegistry[setting.key] && configurationRegistry[setting.key].isExecutable) { - markerData.push({ - severity: MarkerSeverity.Warning, - startLineNumber: setting.keyRange.startLineNumber, - startColumn: setting.keyRange.startColumn, - endLineNumber: setting.keyRange.endLineNumber, - endColumn: setting.keyRange.endColumn, - message: this.getMarkerMessage(setting.key) - }); - } - } - if (this.settingsEditorModel.configurationTarget === ConfigurationTarget.WORKSPACE_FOLDER) { - // Dim and show information for window settings - if (configurationRegistry[setting.key] && configurationRegistry[setting.key].scope === ConfigurationScope.WINDOW) { - ranges.push({ - startLineNumber: setting.keyRange.startLineNumber, - startColumn: setting.keyRange.startColumn - 1, - endLineNumber: setting.valueRange.endLineNumber, - endColumn: setting.valueRange.endColumn - }); - } - } - } - } - } - if (markerData.length) { - this.markerService.changeOne('preferencesEditor', this.settingsEditorModel.uri, markerData); - } else { - this.markerService.remove('preferencesEditor', [this.settingsEditorModel.uri]); - } - this.editor.changeDecorations(changeAccessor => this.decorationIds = changeAccessor.deltaDecorations(this.decorationIds, ranges.map(range => this.createDecoration(range, this.editor.getModel())))); - } - - private createDecoration(range: IRange, model: ITextModel): IModelDeltaDecoration { - return { - range, - options: !this.environmentService.isBuilt || this.environmentService.isExtensionDevelopment ? UnsupportedSettingsRenderer._DIM_CONFIGUARATION_DEV_MODE : UnsupportedSettingsRenderer._DIM_CONFIGUARATION_ - }; - } - - private getMarkerMessage(settingKey: string): string { - switch (settingKey) { - case 'php.validate.executablePath': - return nls.localize('unsupportedPHPExecutablePathSetting', "This setting must be a User Setting. To configure PHP for the workspace, open a PHP file and click on 'PHP Path' in the status bar."); - default: - return nls.localize('unsupportedWorkspaceSetting', "This setting must be a User Setting."); - } - } - - public dispose(): void { - this.markerService.remove('preferencesEditor', [this.settingsEditorModel.uri]); - if (this.decorationIds) { - this.decorationIds = this.editor.changeDecorations(changeAccessor => { - return changeAccessor.deltaDecorations(this.decorationIds, []); - }); - } - super.dispose(); - } - - private static readonly _DIM_CONFIGUARATION_ = ModelDecorationOptions.register({ - stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, - inlineClassName: 'dim-configuration', - beforeContentClassName: 'unsupportedWorkbenhSettingInfo', - hoverMessage: new MarkdownString().appendText(nls.localize('unsupportedWorkbenchSetting', "This setting cannot be applied now. It will be applied when you open this folder directly.")) - }); - - private static readonly _DIM_CONFIGUARATION_DEV_MODE = ModelDecorationOptions.register({ - stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, - inlineClassName: 'dim-configuration', - beforeContentClassName: 'unsupportedWorkbenhSettingInfo', - hoverMessage: new MarkdownString().appendText(nls.localize('unsupportedWorkbenchSettingDevMode', "This setting cannot be applied now. It will be applied if you define it's scope as 'resource' while registering, or when you open this folder directly.")) - }); -} - class WorkspaceConfigurationRenderer extends Disposable { private decorationIds: string[] = []; @@ -1444,8 +1318,6 @@ class WorkspaceConfigurationRenderer extends Disposable { this.associatedSettingsEditorModel = associatedSettingsEditorModel; // Dim other configurations in workspace configuration file only in the context of Settings Editor if (this.associatedSettingsEditorModel && this.workspaceContextService.getWorkbenchState() === WorkbenchState.WORKSPACE && this.workspaceSettingsEditorModel instanceof WorkspaceConfigurationEditorModel) { - this.editor.changeDecorations(changeAccessor => this.decorationIds = changeAccessor.deltaDecorations(this.decorationIds, [])); - const ranges: IRange[] = []; for (const settingsGroup of this.workspaceSettingsEditorModel.configurationGroups) { for (const section of settingsGroup.sections) { @@ -1461,7 +1333,7 @@ class WorkspaceConfigurationRenderer extends Disposable { } } } - this.editor.changeDecorations(changeAccessor => this.decorationIds = changeAccessor.deltaDecorations(this.decorationIds, ranges.map(range => this.createDecoration(range, this.editor.getModel())))); + this.decorationIds = this.editor.deltaDecorations(this.decorationIds, ranges.map(range => this.createDecoration(range, this.editor.getModel()))); } } @@ -1478,11 +1350,7 @@ class WorkspaceConfigurationRenderer extends Disposable { } public dispose(): void { - if (this.decorationIds) { - this.decorationIds = this.editor.changeDecorations(changeAccessor => { - return changeAccessor.deltaDecorations(this.decorationIds, []); - }); - } + this.decorationIds = this.editor.deltaDecorations(this.decorationIds, []); super.dispose(); } } diff --git a/src/vs/workbench/parts/preferences/browser/preferencesWidgets.ts b/src/vs/workbench/parts/preferences/browser/preferencesWidgets.ts index eb553335292..d9d49c7586b 100644 --- a/src/vs/workbench/parts/preferences/browser/preferencesWidgets.ts +++ b/src/vs/workbench/parts/preferences/browser/preferencesWidgets.ts @@ -5,7 +5,7 @@ import { localize } from 'vs/nls'; import URI from 'vs/base/common/uri'; -import { Dimension, $ } from 'vs/base/browser/builder'; +import { $ } from 'vs/base/browser/builder'; import * as DOM from 'vs/base/browser/dom'; import { TPromise } from 'vs/base/common/winjs.base'; import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle'; @@ -17,7 +17,7 @@ import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition, OverlayWidgetPosit import { InputBox, IInputOptions } from 'vs/base/browser/ui/inputbox/inputBox'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IContextViewService, IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { ISettingsGroup } from 'vs/workbench/parts/preferences/common/preferences'; +import { ISettingsGroup } from 'vs/workbench/services/preferences/common/preferences'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IWorkspaceContextService, WorkbenchState, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { IAction, Action } from 'vs/base/common/actions'; @@ -649,7 +649,7 @@ export class SearchWidget extends Widget { this.countElement.style.color = color ? color.toString() : null; } - public layout(dimension: Dimension) { + public layout(dimension: DOM.Dimension) { if (dimension.width < 400) { if (this.countElement) { DOM.addClass(this.countElement, 'hide'); diff --git a/src/vs/workbench/parts/preferences/common/preferences.ts b/src/vs/workbench/parts/preferences/common/preferences.ts index 17f2dae6ffb..556403aa562 100644 --- a/src/vs/workbench/parts/preferences/common/preferences.ts +++ b/src/vs/workbench/parts/preferences/common/preferences.ts @@ -3,21 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { localize } from 'vs/nls'; -import URI from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { IEditor, Position, IEditorOptions } from 'vs/platform/editor/common/editor'; -import { ITextModel } from 'vs/editor/common/model'; -import { IKeybindingItemEntry } from 'vs/workbench/parts/preferences/common/keybindingsEditorModel'; -import { IRange } from 'vs/editor/common/core/range'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { join } from 'vs/base/common/paths'; -import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; -import { Event } from 'vs/base/common/event'; -import { IStringDictionary } from 'vs/base/common/collections'; -import { ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { ISettingsEditorModel, ISearchResult } from 'vs/workbench/services/preferences/common/preferences'; +import { IEditor } from 'vs/platform/editor/common/editor'; +import { IKeybindingItemEntry } from 'vs/workbench/services/preferences/common/keybindingsEditorModel'; export interface IWorkbenchSettingsConfiguration { workbench: { @@ -33,164 +25,6 @@ export interface IWorkbenchSettingsConfiguration { }; } -export interface ISettingsGroup { - id: string; - range: IRange; - title: string; - titleRange: IRange; - sections: ISettingsSection[]; -} - -export interface ISettingsSection { - titleRange?: IRange; - title?: string; - settings: ISetting[]; -} - -export interface ISetting { - range: IRange; - key: string; - keyRange: IRange; - value: any; - valueRange: IRange; - description: string[]; - descriptionRanges: IRange[]; - overrides?: ISetting[]; - overrideOf?: ISetting; -} - -export interface IExtensionSetting extends ISetting { - extensionName: string; - extensionPublisher: string; -} - -export interface ISearchResult { - filterMatches: ISettingMatch[]; - metadata?: IFilterMetadata; -} - -export interface ISearchResultGroup { - id: string; - label: string; - result: ISearchResult; - order: number; -} - -export interface IFilterResult { - query?: string; - filteredGroups: ISettingsGroup[]; - allGroups: ISettingsGroup[]; - matches: IRange[]; - metadata?: IStringDictionary; -} - -export interface ISettingMatch { - setting: ISetting; - matches: IRange[]; - score: number; -} - -export interface IScoredResults { - [key: string]: IRemoteSetting; -} - -export interface IRemoteSetting { - score: number; - key: string; - id: string; - defaultValue: string; - description: string; - packageId: string; - extensionName?: string; - extensionPublisher?: string; -} - -export interface IFilterMetadata { - requestUrl: string; - requestBody: string; - timestamp: number; - duration: number; - scoredResults: IScoredResults; - extensions?: ILocalExtension[]; - - /** The number of requests made, since requests are split by number of filters */ - requestCount?: number; - - /** The name of the server that actually served the request */ - context: string; -} - -export interface IPreferencesEditorModel { - uri: URI; - getPreference(key: string): T; - dispose(): void; -} - -export type IGroupFilter = (group: ISettingsGroup) => boolean; -export type ISettingMatcher = (setting: ISetting, group: ISettingsGroup) => { matches: IRange[], score: number }; - -export interface ISettingsEditorModel extends IPreferencesEditorModel { - readonly onDidChangeGroups: Event; - settingsGroups: ISettingsGroup[]; - filterSettings(filter: string, groupFilter: IGroupFilter, settingMatcher: ISettingMatcher): ISettingMatch[]; - findValueMatches(filter: string, setting: ISetting): IRange[]; - updateResultGroup(id: string, resultGroup: ISearchResultGroup): IFilterResult; -} - -export interface IKeybindingsEditorModel extends IPreferencesEditorModel { -} - -export const IPreferencesService = createDecorator('preferencesService'); - -export interface IPreferencesService { - _serviceBrand: any; - - userSettingsResource: URI; - workspaceSettingsResource: URI; - getFolderSettingsResource(resource: URI): URI; - - resolveModel(uri: URI): TPromise; - createPreferencesEditorModel(uri: URI): TPromise>; - - openRawDefaultSettings(): TPromise; - openGlobalSettings(options?: IEditorOptions, position?: Position): TPromise; - openWorkspaceSettings(options?: IEditorOptions, position?: Position): TPromise; - openFolderSettings(folder: URI, options?: IEditorOptions, position?: Position): TPromise; - switchSettings(target: ConfigurationTarget, resource: URI): TPromise; - openGlobalKeybindingSettings(textual: boolean): TPromise; - - configureSettingsForLanguage(language: string): void; -} - - -export interface IKeybindingsEditor extends IEditor { - - activeKeybindingEntry: IKeybindingItemEntry; - - search(filter: string): void; - clearSearchResults(): void; - focusKeybindings(): void; - defineKeybinding(keybindingEntry: IKeybindingItemEntry): TPromise; - removeKeybinding(keybindingEntry: IKeybindingItemEntry): TPromise; - resetKeybinding(keybindingEntry: IKeybindingItemEntry): TPromise; - copyKeybinding(keybindingEntry: IKeybindingItemEntry): TPromise; - copyKeybindingCommand(keybindingEntry: IKeybindingItemEntry): TPromise; - showSimilarKeybindings(keybindingEntry: IKeybindingItemEntry): TPromise; -} - -export function getSettingsTargetName(target: ConfigurationTarget, resource: URI, workspaceContextService: IWorkspaceContextService): string { - switch (target) { - case ConfigurationTarget.USER: - return localize('userSettingsTarget', "User Settings"); - case ConfigurationTarget.WORKSPACE: - return localize('workspaceSettingsTarget', "Workspace Settings"); - case ConfigurationTarget.WORKSPACE_FOLDER: - const folder = workspaceContextService.getWorkspaceFolder(resource); - return folder ? folder.name : ''; - } - return ''; -} - export interface IEndpointDetails { urlBase: string; key?: string; @@ -209,6 +43,21 @@ export interface ISearchProvider { searchModel(preferencesModel: ISettingsEditorModel): TPromise; } +export interface IKeybindingsEditor extends IEditor { + + activeKeybindingEntry: IKeybindingItemEntry; + + search(filter: string): void; + clearSearchResults(): void; + focusKeybindings(): void; + defineKeybinding(keybindingEntry: IKeybindingItemEntry): TPromise; + removeKeybinding(keybindingEntry: IKeybindingItemEntry): TPromise; + resetKeybinding(keybindingEntry: IKeybindingItemEntry): TPromise; + copyKeybinding(keybindingEntry: IKeybindingItemEntry): TPromise; + copyKeybindingCommand(keybindingEntry: IKeybindingItemEntry): TPromise; + showSimilarKeybindings(keybindingEntry: IKeybindingItemEntry): TPromise; +} + export const CONTEXT_SETTINGS_EDITOR = new RawContextKey('inSettingsEditor', false); export const CONTEXT_SETTINGS_SEARCH_FOCUS = new RawContextKey('inSettingsSearch', false); export const CONTEXT_KEYBINDINGS_EDITOR = new RawContextKey('inKeybindings', false); diff --git a/src/vs/workbench/parts/preferences/common/preferencesContribution.ts b/src/vs/workbench/parts/preferences/common/preferencesContribution.ts index adf1fc4a589..09d6aa2c742 100644 --- a/src/vs/workbench/parts/preferences/common/preferencesContribution.ts +++ b/src/vs/workbench/parts/preferences/common/preferencesContribution.ts @@ -13,7 +13,7 @@ import * as JSONContributionRegistry from 'vs/platform/jsonschemas/common/jsonCo import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; -import { IPreferencesService, FOLDER_SETTINGS_PATH, DEFAULT_SETTINGS_EDITOR_SETTING } from 'vs/workbench/parts/preferences/common/preferences'; +import { IPreferencesService, FOLDER_SETTINGS_PATH, DEFAULT_SETTINGS_EDITOR_SETTING } from 'vs/workbench/services/preferences/common/preferences'; import { dispose, IDisposable } from 'vs/base/common/lifecycle'; import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService'; import { endsWith } from 'vs/base/common/strings'; diff --git a/src/vs/workbench/parts/preferences/electron-browser/preferences.contribution.ts b/src/vs/workbench/parts/preferences/electron-browser/preferences.contribution.ts index 50c0e829378..aa1599d77e3 100644 --- a/src/vs/workbench/parts/preferences/electron-browser/preferences.contribution.ts +++ b/src/vs/workbench/parts/preferences/electron-browser/preferences.contribution.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; +import 'vs/css!../browser/media/preferences'; import * as nls from 'vs/nls'; import URI from 'vs/base/common/uri'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -14,14 +15,14 @@ import { SyncActionDescriptor, MenuRegistry, MenuId } from 'vs/platform/actions/ import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { KeyMod, KeyChord, KeyCode } from 'vs/base/common/keyCodes'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; -import { DefaultPreferencesEditorInput, PreferencesEditor, PreferencesEditorInput } from 'vs/workbench/parts/preferences/browser/preferencesEditor'; -import { KeybindingsEditor, KeybindingsEditorInput } from 'vs/workbench/parts/preferences/browser/keybindingsEditor'; -import { OpenRawDefaultSettingsAction, OpenGlobalSettingsAction, OpenGlobalKeybindingsFileAction, OpenWorkspaceSettingsAction, OpenFolderSettingsAction, ConfigureLanguageBasedSettingsAction, OPEN_FOLDER_SETTINGS_COMMAND, OpenGlobalKeybindingsAction } from 'vs/workbench/parts/preferences/browser/preferencesActions'; +import { PreferencesEditor } from 'vs/workbench/parts/preferences/browser/preferencesEditor'; +import { DefaultPreferencesEditorInput, PreferencesEditorInput, KeybindingsEditorInput } from 'vs/workbench/services/preferences/common/preferencesEditorInput'; +import { KeybindingsEditor } from 'vs/workbench/parts/preferences/browser/keybindingsEditor'; +import { OpenRawDefaultSettingsAction, OpenSettingsAction, OpenGlobalSettingsAction, OpenGlobalKeybindingsFileAction, OpenWorkspaceSettingsAction, OpenFolderSettingsAction, ConfigureLanguageBasedSettingsAction, OPEN_FOLDER_SETTINGS_COMMAND, OpenGlobalKeybindingsAction } from 'vs/workbench/parts/preferences/browser/preferencesActions'; import { - IPreferencesService, IKeybindingsEditor, IPreferencesSearchService, CONTEXT_KEYBINDING_FOCUS, CONTEXT_KEYBINDINGS_EDITOR, CONTEXT_KEYBINDINGS_SEARCH_FOCUS, KEYBINDINGS_EDITOR_COMMAND_DEFINE, KEYBINDINGS_EDITOR_COMMAND_REMOVE, KEYBINDINGS_EDITOR_COMMAND_SEARCH, + IKeybindingsEditor, IPreferencesSearchService, CONTEXT_KEYBINDING_FOCUS, CONTEXT_KEYBINDINGS_EDITOR, CONTEXT_KEYBINDINGS_SEARCH_FOCUS, KEYBINDINGS_EDITOR_COMMAND_DEFINE, KEYBINDINGS_EDITOR_COMMAND_REMOVE, KEYBINDINGS_EDITOR_COMMAND_SEARCH, KEYBINDINGS_EDITOR_COMMAND_COPY, KEYBINDINGS_EDITOR_COMMAND_RESET, KEYBINDINGS_EDITOR_COMMAND_COPY_COMMAND, KEYBINDINGS_EDITOR_COMMAND_SHOW_SIMILAR, KEYBINDINGS_EDITOR_COMMAND_FOCUS_KEYBINDINGS, KEYBINDINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS } from 'vs/workbench/parts/preferences/common/preferences'; -import { PreferencesService } from 'vs/workbench/parts/preferences/browser/preferencesService'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; import { PreferencesContribution } from 'vs/workbench/parts/preferences/common/preferencesContribution'; @@ -31,8 +32,8 @@ import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { IEditorRegistry, EditorDescriptor, Extensions as EditorExtensions } from 'vs/workbench/browser/editor'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { PreferencesSearchService } from 'vs/workbench/parts/preferences/electron-browser/preferencesSearch'; +import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; -registerSingleton(IPreferencesService, PreferencesService); registerSingleton(IPreferencesSearchService, PreferencesSearchService); Registry.as(EditorExtensions.Editors).registerEditor( @@ -165,7 +166,8 @@ Registry.as(EditorInputExtensions.EditorInputFactor const category = nls.localize('preferences', "Preferences"); const registry = Registry.as(Extensions.WorkbenchActions); registry.registerWorkbenchAction(new SyncActionDescriptor(OpenRawDefaultSettingsAction, OpenRawDefaultSettingsAction.ID, OpenRawDefaultSettingsAction.LABEL), 'Preferences: Open Raw Default Settings', category); -registry.registerWorkbenchAction(new SyncActionDescriptor(OpenGlobalSettingsAction, OpenGlobalSettingsAction.ID, OpenGlobalSettingsAction.LABEL, { primary: KeyMod.CtrlCmd | KeyCode.US_COMMA }), 'Preferences: Open User Settings', category); +registry.registerWorkbenchAction(new SyncActionDescriptor(OpenSettingsAction, OpenSettingsAction.ID, OpenSettingsAction.LABEL, { primary: KeyMod.CtrlCmd | KeyCode.US_COMMA }), 'Preferences: Open Settings', category); +registry.registerWorkbenchAction(new SyncActionDescriptor(OpenGlobalSettingsAction, OpenGlobalSettingsAction.ID, OpenGlobalSettingsAction.LABEL), 'Preferences: Open User Settings', category); registry.registerWorkbenchAction(new SyncActionDescriptor(OpenGlobalKeybindingsAction, OpenGlobalKeybindingsAction.ID, OpenGlobalKeybindingsAction.LABEL, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_S) }), 'Preferences: Open Keyboard Shortcuts', category); registry.registerWorkbenchAction(new SyncActionDescriptor(OpenGlobalKeybindingsFileAction, OpenGlobalKeybindingsFileAction.ID, OpenGlobalKeybindingsFileAction.LABEL, { primary: null }), 'Preferences: Open Keyboard Shortcuts File', category); registry.registerWorkbenchAction(new SyncActionDescriptor(ConfigureLanguageBasedSettingsAction, ConfigureLanguageBasedSettingsAction.ID, ConfigureLanguageBasedSettingsAction.LABEL), 'Preferences: Configure Language Specific Settings...', category); diff --git a/src/vs/workbench/parts/preferences/electron-browser/preferencesSearch.ts b/src/vs/workbench/parts/preferences/electron-browser/preferencesSearch.ts index aa9985fa576..3b40c62f656 100644 --- a/src/vs/workbench/parts/preferences/electron-browser/preferencesSearch.ts +++ b/src/vs/workbench/parts/preferences/electron-browser/preferencesSearch.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { TPromise } from 'vs/base/common/winjs.base'; -import { ISettingsEditorModel, ISetting, ISettingsGroup, IWorkbenchSettingsConfiguration, IFilterMetadata, IPreferencesSearchService, ISearchResult, ISearchProvider, IGroupFilter, ISettingMatcher, IScoredResults, ISettingMatch, IRemoteSetting, IExtensionSetting } from 'vs/workbench/parts/preferences/common/preferences'; +import { ISettingsEditorModel, ISetting, ISettingsGroup, IFilterMetadata, ISearchResult, IGroupFilter, ISettingMatcher, IScoredResults, ISettingMatch, IRemoteSetting, IExtensionSetting } from 'vs/workbench/services/preferences/common/preferences'; import { IRange } from 'vs/editor/common/core/range'; import { distinct, top } from 'vs/base/common/arrays'; import * as strings from 'vs/base/common/strings'; @@ -20,6 +20,7 @@ import { asJson } from 'vs/base/node/request'; import { Disposable } from 'vs/base/common/lifecycle'; import { IExtensionManagementService, LocalExtensionType, ILocalExtension, IExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ILogService } from 'vs/platform/log/common/log'; +import { IPreferencesSearchService, ISearchProvider, IWorkbenchSettingsConfiguration } from 'vs/workbench/parts/preferences/common/preferences'; export interface IEndpointDetails { urlBase: string; @@ -45,7 +46,7 @@ export class PreferencesSearchService extends Disposable implements IPreferences // Filter to enabled extensions that have settings return exts .filter(ext => this.extensionEnablementService.isEnabled(ext)) - .filter(ext => ext.manifest.contributes && ext.manifest.contributes.configuration) + .filter(ext => ext.manifest && ext.manifest.contributes && ext.manifest.contributes.configuration) .filter(ext => !!ext.identifier.uuid); }); } diff --git a/src/vs/workbench/parts/quickopen/browser/viewPickerHandler.ts b/src/vs/workbench/parts/quickopen/browser/viewPickerHandler.ts index 6c164a8125b..d349a73809e 100644 --- a/src/vs/workbench/parts/quickopen/browser/viewPickerHandler.ts +++ b/src/vs/workbench/parts/quickopen/browser/viewPickerHandler.ts @@ -21,9 +21,6 @@ import { fuzzyContains, stripWildcards } from 'vs/base/common/strings'; import { matchesFuzzy } from 'vs/base/common/filters'; import { ViewsRegistry, ViewLocation, IViewsViewlet } from 'vs/workbench/common/views'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { VIEWLET_ID as EXPLORER_VIEWLET_ID } from 'vs/workbench/parts/files/common/files'; -import { VIEWLET_ID as DEBUG_VIEWLET_ID } from 'vs/workbench/parts/debug/common/debug'; -import { VIEWLET_ID as EXTENSIONS_VIEWLET_ID } from 'vs/workbench/parts/extensions/common/extensions'; import { ViewletDescriptor } from 'vs/workbench/browser/viewlet'; export const VIEW_PICKER_PREFIX = 'view '; @@ -146,11 +143,7 @@ export class ViewPickerHandler extends QuickOpenHandler { // Views viewlets.forEach((viewlet, index) => { - const viewLocation: ViewLocation = viewlet.id === EXPLORER_VIEWLET_ID ? ViewLocation.Explorer - : viewlet.id === DEBUG_VIEWLET_ID ? ViewLocation.Debug - : viewlet.id === EXTENSIONS_VIEWLET_ID ? ViewLocation.Extensions - : null; - + const viewLocation: ViewLocation = ViewLocation.get(viewlet.id); if (viewLocation) { const viewEntriesForViewlet: ViewEntry[] = getViewEntriesForViewlet(viewlet, viewLocation); viewEntries.push(...viewEntriesForViewlet); 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 0b63e5adb18..9981d15cb98 100644 --- a/src/vs/workbench/parts/relauncher/electron-browser/relauncher.contribution.ts +++ b/src/vs/workbench/parts/relauncher/electron-browser/relauncher.contribution.ts @@ -17,15 +17,17 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { RunOnceScheduler } from 'vs/base/common/async'; import URI from 'vs/base/common/uri'; import { isEqual } from 'vs/base/common/resources'; -import { isLinux } from 'vs/base/common/platform'; +import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { equals } from 'vs/base/common/objects'; interface IConfiguration extends IWindowsConfiguration { update: { channel: string; }; telemetry: { enableCrashReporter: boolean }; keyboard: { touchbar: { enabled: boolean } }; workbench: { tree: { horizontalScrolling: boolean } }; + files: { useExperimentalFileWatcher: boolean, watcherExclude: object }; } export class SettingsChangeRelauncher implements IWorkbenchContribution { @@ -34,11 +36,14 @@ export class SettingsChangeRelauncher implements IWorkbenchContribution { private titleBarStyle: 'native' | 'custom'; private nativeTabs: boolean; + private clickThroughInactive: boolean; private updateChannel: string; private enableCrashReporter: boolean; private touchbarEnabled: boolean; private treeHorizontalScrolling: boolean; private windowsSmoothScrollingWorkaround: boolean; + private experimentalFileWatcher: boolean; + private fileWatcherExclude: object; private firstFolderResource: URI; private extensionHostRestarter: RunOnceScheduler; @@ -72,18 +77,24 @@ export class SettingsChangeRelauncher implements IWorkbenchContribution { private onConfigurationChange(config: IConfiguration, notify: boolean): void { let changed = false; - // Titlebar style - if (config.window && config.window.titleBarStyle !== this.titleBarStyle && (config.window.titleBarStyle === 'native' || config.window.titleBarStyle === 'custom')) { + // macOS: Titlebar style + if (isMacintosh && config.window && config.window.titleBarStyle !== this.titleBarStyle && (config.window.titleBarStyle === 'native' || config.window.titleBarStyle === 'custom')) { this.titleBarStyle = config.window.titleBarStyle; changed = true; } - // Native tabs - if (config.window && typeof config.window.nativeTabs === 'boolean' && config.window.nativeTabs !== this.nativeTabs) { + // macOS: Native tabs + if (isMacintosh && config.window && typeof config.window.nativeTabs === 'boolean' && config.window.nativeTabs !== this.nativeTabs) { this.nativeTabs = config.window.nativeTabs; changed = true; } + // macOS: Click through (accept first mouse) + if (isMacintosh && config.window && typeof config.window.clickThroughInactive === 'boolean' && config.window.clickThroughInactive !== this.clickThroughInactive) { + this.clickThroughInactive = config.window.clickThroughInactive; + changed = true; + } + // Update channel if (config.update && typeof config.update.channel === 'string' && config.update.channel !== this.updateChannel) { this.updateChannel = config.update.channel; @@ -96,8 +107,22 @@ export class SettingsChangeRelauncher implements IWorkbenchContribution { changed = true; } - // Touchbar config - if (config.keyboard && config.keyboard.touchbar && typeof config.keyboard.touchbar.enabled === 'boolean' && config.keyboard.touchbar.enabled !== this.touchbarEnabled) { + // Experimental File Watcher + if (config.files && typeof config.files.useExperimentalFileWatcher === 'boolean' && config.files.useExperimentalFileWatcher !== this.experimentalFileWatcher) { + this.experimentalFileWatcher = config.files.useExperimentalFileWatcher; + changed = true; + } + + // File Watcher Excludes (only if in folder workspace mode) + if (!this.experimentalFileWatcher && this.contextService.getWorkbenchState() === WorkbenchState.FOLDER) { + if (config.files && typeof config.files.watcherExclude === 'object' && !equals(config.files.watcherExclude, this.fileWatcherExclude)) { + this.fileWatcherExclude = config.files.watcherExclude; + changed = true; + } + } + + // macOS: Touchbar config + if (isMacintosh && config.keyboard && config.keyboard.touchbar && typeof config.keyboard.touchbar.enabled === 'boolean' && config.keyboard.touchbar.enabled !== this.touchbarEnabled) { this.touchbarEnabled = config.keyboard.touchbar.enabled; changed = true; } @@ -109,7 +134,7 @@ export class SettingsChangeRelauncher implements IWorkbenchContribution { } // Windows: smooth scrolling workaround - if (config.window && typeof config.window.smoothScrollingWorkaround === 'boolean' && config.window.smoothScrollingWorkaround !== this.windowsSmoothScrollingWorkaround) { + if (isWindows && config.window && typeof config.window.smoothScrollingWorkaround === 'boolean' && config.window.smoothScrollingWorkaround !== this.windowsSmoothScrollingWorkaround) { this.windowsSmoothScrollingWorkaround = config.window.smoothScrollingWorkaround; changed = true; } diff --git a/src/vs/workbench/parts/scm/electron-browser/scm.contribution.ts b/src/vs/workbench/parts/scm/electron-browser/scm.contribution.ts index 9b3b978ffcf..95bc4991d0f 100644 --- a/src/vs/workbench/parts/scm/electron-browser/scm.contribution.ts +++ b/src/vs/workbench/parts/scm/electron-browser/scm.contribution.ts @@ -39,7 +39,7 @@ const viewletDescriptor = new ViewletDescriptor( VIEWLET_ID, localize('source control', "Source Control"), 'scm', - 36 + 2 ); Registry.as(ViewletExtensions.Viewlets) diff --git a/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts b/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts index b320d1e7db5..17c764c629e 100644 --- a/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts +++ b/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts @@ -8,14 +8,13 @@ import 'vs/css!./media/scmViewlet'; import { localize } from 'vs/nls'; import { TPromise } from 'vs/base/common/winjs.base'; -import { Event, Emitter, chain, mapEvent, anyEvent, filterEvent } from 'vs/base/common/event'; +import { Event, Emitter, chain, mapEvent, anyEvent, filterEvent, latch } from 'vs/base/common/event'; import { domEvent, stop } from 'vs/base/browser/event'; import { basename } from 'vs/base/common/paths'; import { onUnexpectedError } from 'vs/base/common/errors'; import { IDisposable, dispose, combinedDisposable, empty as EmptyDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { Builder, Dimension } from 'vs/base/browser/builder'; import { PanelViewlet, ViewletPanel } from 'vs/workbench/browser/parts/views/panelViewlet'; -import { append, $, addClass, toggleClass, trackFocus } from 'vs/base/browser/dom'; +import { append, $, addClass, toggleClass, trackFocus, Dimension, addDisposableListener } from 'vs/base/browser/dom'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { List } from 'vs/base/browser/ui/list/listWidget'; import { IDelegate, IRenderer, IListContextMenuEvent, IListEvent } from 'vs/base/browser/ui/list/list'; @@ -58,6 +57,10 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { ThrottledDelayer } from 'vs/base/common/async'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IPartService } from 'vs/workbench/services/part/common/partService'; +import { IViewDescriptorRef, PersistentContributableViewsModel, IAddedViewDescriptorRef } from 'vs/workbench/browser/parts/views/contributableViews'; +import { ViewLocation, IViewDescriptor } from 'vs/workbench/common/views'; +import { ViewsViewletPanel } from 'vs/workbench/browser/parts/views/viewsViewlet'; +import { IPanelDndController, Panel } from '../../../../base/browser/ui/splitview/panelview'; export interface ISpliceEvent { index: number; @@ -1024,6 +1027,17 @@ class InstallAdditionalSCMProvidersAction extends Action { } } +class SCMPanelDndController implements IPanelDndController { + + canDrag(panel: Panel): boolean { + return !(panel instanceof MainPanel) && !(panel instanceof RepositoryPanel); + } + + canDrop(panel: Panel, overPanel: Panel): boolean { + return !(overPanel instanceof MainPanel) && !(overPanel instanceof RepositoryPanel); + } +} + export class SCMViewlet extends PanelViewlet implements IViewModel { private el: HTMLElement; @@ -1048,6 +1062,9 @@ export class SCMViewlet extends PanelViewlet implements IViewModel { get repositories(): ISCMRepository[] { return this._repositories; } get selectedRepositories(): ISCMRepository[] { return this.repositoryPanels.map(p => p.repository); } + private contributedViews: PersistentContributableViewsModel; + private contributedViewDisposables: IDisposable[] = []; + constructor( @IPartService partService: IPartService, @ITelemetryService telemetryService: ITelemetryService, @@ -1067,19 +1084,22 @@ export class SCMViewlet extends PanelViewlet implements IViewModel { @IExtensionService extensionService: IExtensionService, @IConfigurationService private configurationService: IConfigurationService, ) { - super(VIEWLET_ID, { showHeaderInTitleWhenSingleView: true }, partService, contextMenuService, telemetryService, themeService); + super(VIEWLET_ID, { showHeaderInTitleWhenSingleView: true, dnd: new SCMPanelDndController() }, partService, contextMenuService, telemetryService, themeService); this.menus = instantiationService.createInstance(SCMMenus, undefined); this.menus.onDidChangeTitle(this.updateTitleArea, this, this.disposables); + + this.contributedViews = new PersistentContributableViewsModel(ViewLocation.SCM, 'scm.views', contextKeyService, storageService, contextService); + this.disposables.push(this.contributedViews); } - async create(parent: Builder): TPromise { + async create(parent: HTMLElement): TPromise { await super.create(parent); - this.el = parent.getHTMLElement(); + this.el = parent; addClass(this.el, 'scm-viewlet'); addClass(this.el, 'empty'); - append(parent.getHTMLElement(), $('div.empty-message', null, localize('no open repo', "There are no active source control providers."))); + append(parent, $('div.empty-message', null, localize('no open repo', "There are no active source control providers."))); this.scmService.onDidAddRepository(this.onDidAddRepository, this, this.disposables); this.scmService.onDidRemoveRepository(this.onDidRemoveRepository, this, this.disposables); @@ -1089,6 +1109,19 @@ export class SCMViewlet extends PanelViewlet implements IViewModel { onDidUpdateConfiguration(this.onDidChangeRepositories, this, this.disposables); this.onDidChangeRepositories(); + + this.contributedViews.onDidAdd(this.onDidAddContributedView, this, this.disposables); + this.contributedViews.onDidRemove(this.onDidRemoveContributedView, this, this.disposables); + + let index = this.getContributedViewsStartIndex(); + for (const viewDescriptor of this.contributedViews.visibleViewDescriptors) { + const size = this.contributedViews.getSize(viewDescriptor.id); + const collapsed = this.contributedViews.isCollapsed(viewDescriptor.id); + + this.onDidAddContributedView({ viewDescriptor, index: index++, size, collapsed }); + } + + this.onDidSashChange(this.saveContributedViewSizes, this, this.disposables); } private onDidAddRepository(repository: ISCMRepository): void { @@ -1130,13 +1163,13 @@ export class SCMViewlet extends PanelViewlet implements IViewModel { if (shouldMainPanelBeVisible) { this.mainPanel = this.instantiationService.createInstance(MainPanel, this); - this.addPanel(this.mainPanel, this.mainPanel.minimumSize, 0); + this.addPanels([{ panel: this.mainPanel, size: this.mainPanel.minimumSize, index: 0 }]); const selectionChangeDisposable = this.mainPanel.onSelectionChange(this.onSelectionChange, this); this.onSelectionChange(this.mainPanel.getSelection()); this.mainPanelDisposable = toDisposable(() => { - this.removePanel(this.mainPanel); + this.removePanels([this.mainPanel]); selectionChangeDisposable.dispose(); this.mainPanel.dispose(); }); @@ -1147,15 +1180,28 @@ export class SCMViewlet extends PanelViewlet implements IViewModel { } } + private getContributedViewsStartIndex(): number { + return (this.mainPanel ? 1 : 0) + this.repositoryPanels.length; + } + setVisible(visible: boolean): TPromise { - const result = super.setVisible(visible); + const promises: TPromise[] = []; + promises.push(super.setVisible(visible)); if (!visible) { this.cachedMainPanelHeight = this.getPanelSize(this.mainPanel); } this._onDidChangeVisibility.fire(visible); - return result; + + const start = this.getContributedViewsStartIndex(); + + for (let i = 0; i < this.contributedViews.viewDescriptors.length; i++) { + const panel = this.panels[start + i] as ViewsViewletPanel; + promises.push(panel.setVisible(visible)); + } + + return TPromise.join(promises) as TPromise; } getOptimalWidth(): number { @@ -1175,8 +1221,7 @@ export class SCMViewlet extends PanelViewlet implements IViewModel { getActions(): IAction[] { if (this.isSingleView()) { - const [panel] = this.repositoryPanels; - return panel.getActions(); + return this.panels[0].getActions(); } return this.menus.getTitleActions(); @@ -1186,7 +1231,7 @@ export class SCMViewlet extends PanelViewlet implements IViewModel { let result: IAction[]; if (this.isSingleView()) { - const [panel] = this.repositoryPanels; + const [panel] = this.panels; result = [ ...panel.getSecondaryActions(), @@ -1213,13 +1258,33 @@ export class SCMViewlet extends PanelViewlet implements IViewModel { return new ContextAwareMenuItemActionItem(action, this.keybindingService, this.notificationService, this.contextMenuService); } + private didLayout = false; layout(dimension: Dimension): void { super.layout(dimension); this._height = dimension.height; + + if (this.didLayout) { + // this.saveViewSizes(); + } else { + this.didLayout = true; + this.restoreContributedViewSizes(); + } + } + + movePanel(from: ViewletPanel, to: ViewletPanel): void { + const start = this.getContributedViewsStartIndex(); + const fromIndex = firstIndex(this.panels, panel => panel === from) - start; + const toIndex = firstIndex(this.panels, panel => panel === to) - start; + const fromViewDescriptor = this.contributedViews.viewDescriptors[fromIndex]; + const toViewDescriptor = this.contributedViews.viewDescriptors[toIndex]; + + super.movePanel(from, to); + this.contributedViews.move(fromViewDescriptor.id, toViewDescriptor.id); } private onSelectionChange(repositories: ISCMRepository[]): void { const wasSingleView = this.isSingleView(); + const contributableViewsHeight = this.getContributableViewsSize(); // Collect unselected panels const panelsToRemove = this.repositoryPanels @@ -1235,14 +1300,15 @@ export class SCMViewlet extends PanelViewlet implements IViewModel { .map(r => this.instantiationService.createInstance(RepositoryPanel, r, this)); // Add new selected panels + let index = repositoryPanels.length + (this.mainPanel ? 1 : 0); this.repositoryPanels = [...repositoryPanels, ...newRepositoryPanels]; newRepositoryPanels.forEach(panel => { - this.addPanel(panel, panel.minimumSize, this.length); + this.addPanels([{ panel, size: panel.minimumSize, index: index++ }]); panel.repository.focus(); }); // Remove unselected panels - panelsToRemove.forEach(panel => this.removePanel(panel)); + this.removePanels(panelsToRemove); // Restore main panel height if (this.isVisible() && typeof this.cachedMainPanelHeight === 'number') { @@ -1253,11 +1319,14 @@ export class SCMViewlet extends PanelViewlet implements IViewModel { // Resize all panels equally const height = typeof this.height === 'number' ? this.height : 1000; const mainPanelHeight = this.getPanelSize(this.mainPanel); - const size = (height - mainPanelHeight) / repositories.length; + const size = (height - mainPanelHeight - contributableViewsHeight) / repositories.length; for (const panel of this.repositoryPanels) { this.resizePanel(panel, size); } + // Resize contributed view sizes + this.restoreContributedViewSizes(); + // React to menu changes for single view mode if (wasSingleView !== this.isSingleView()) { this.singleRepositoryPanelTitleActionsDisposable.dispose(); @@ -1270,8 +1339,123 @@ export class SCMViewlet extends PanelViewlet implements IViewModel { } } + private getContributableViewsSize(): number { + let value = 0; + + for (let i = this.getContributedViewsStartIndex(); i < this.length; i++) { + value += this.getPanelSize(this.panels[i]); + } + + return value; + } + + onDidAddContributedView({ viewDescriptor, index, size, collapsed }: IAddedViewDescriptorRef): void { + const start = this.getContributedViewsStartIndex(); + const panel = this.instantiationService.createInstance(viewDescriptor.ctor, { + id: viewDescriptor.id, + name: viewDescriptor.name, + actionRunner: this.getActionRunner(), + expanded: !collapsed, + viewletSettings: {} // what is this + }) as ViewsViewletPanel; + + this.addPanels([{ panel, size: size || panel.minimumSize, index: start + index }]); + panel.setVisible(true); + + const contextMenuDisposable = addDisposableListener(panel.draggableElement, 'contextmenu', e => { + e.stopPropagation(); + e.preventDefault(); + this.onViewHeaderContextMenu(new StandardMouseEvent(e), viewDescriptor); + }); + + const collapseDisposable = latch(mapEvent(panel.onDidChange, () => !panel.isExpanded()))(collapsed => { + this.contributedViews.setCollapsed(viewDescriptor.id, collapsed); + }); + + this.contributedViewDisposables.splice(index, 0, combinedDisposable([contextMenuDisposable, collapseDisposable])); + } + + private onViewHeaderContextMenu(event: StandardMouseEvent, viewDescriptor: IViewDescriptor): void { + const actions: IAction[] = []; + actions.push({ + id: `${viewDescriptor.id}.removeView`, + label: localize('hideView', "Hide"), + enabled: viewDescriptor.canToggleVisibility, + run: () => this.contributedViews.setVisible(viewDescriptor.id, !this.contributedViews.isVisible(viewDescriptor.id)) + }); + + const otherActions = this.getContextMenuActions(); + if (otherActions.length) { + actions.push(...[new Separator(), ...otherActions]); + } + + let anchor: { x: number, y: number } = { x: event.posx, y: event.posy }; + this.contextMenuService.showContextMenu({ + getAnchor: () => anchor, + getActions: () => TPromise.as(actions) + }); + } + + getContextMenuActions(): IAction[] { + const result: IAction[] = []; + const viewToggleActions = this.contributedViews.viewDescriptors.map(viewDescriptor => ({ + id: `${viewDescriptor.id}.toggleVisibility`, + label: viewDescriptor.name, + checked: this.contributedViews.isVisible(viewDescriptor.id), + enabled: viewDescriptor.canToggleVisibility, + run: () => this.contributedViews.setVisible(viewDescriptor.id, !this.contributedViews.isVisible(viewDescriptor.id)) + })); + + result.push(...viewToggleActions); + const parentActions = super.getContextMenuActions(); + if (viewToggleActions.length && parentActions.length) { + result.push(new Separator()); + } + result.push(...parentActions); + return result; + } + + onDidRemoveContributedView({ viewDescriptor, index }: IViewDescriptorRef): void { + const start = this.getContributedViewsStartIndex(); + const panel = this.panels[start + index]; + + this.removePanels([panel]); + + const [disposable] = this.contributedViewDisposables.splice(index, 1); + disposable.dispose(); + } + + private saveContributedViewSizes(): void { + const start = this.getContributedViewsStartIndex(); + + for (let i = 0; i < this.contributedViews.viewDescriptors.length; i++) { + const viewDescriptor = this.contributedViews.viewDescriptors[i]; + const size = this.getPanelSize(this.panels[start + i]); + + this.contributedViews.setSize(viewDescriptor.id, size); + } + } + + private restoreContributedViewSizes(): void { + if (!this.didLayout) { + return; + } + + const start = this.getContributedViewsStartIndex(); + + for (let i = 0; i < this.contributedViews.viewDescriptors.length; i++) { + const panel = this.panels[start + i]; + const viewDescriptor = this.contributedViews.viewDescriptors[i]; + const size = this.contributedViews.getSize(viewDescriptor.id); + + if (typeof size === 'number') { + this.resizePanel(panel, size); + } + } + } + protected isSingleView(): boolean { - return super.isSingleView() && this.repositoryPanels.length === 1; + return super.isSingleView() && this.repositoryPanels.length + this.contributedViews.visibleViewDescriptors.length === 1; } hide(repository: ISCMRepository): void { @@ -1282,8 +1466,14 @@ export class SCMViewlet extends PanelViewlet implements IViewModel { this.mainPanel.hide(repository); } + shutdown(): void { + this.contributedViews.saveViewsStates(); + super.shutdown(); + } + dispose(): void { this.disposables = dispose(this.disposables); + this.contributedViewDisposables = dispose(this.contributedViewDisposables); this.mainPanelDisposable.dispose(); super.dispose(); } diff --git a/src/vs/workbench/parts/search/browser/media/searchview.css b/src/vs/workbench/parts/search/browser/media/searchview.css index f8c247cb778..11d4a566ac9 100644 --- a/src/vs/workbench/parts/search/browser/media/searchview.css +++ b/src/vs/workbench/parts/search/browser/media/searchview.css @@ -179,10 +179,15 @@ padding: 0; } -.search-view .monaco-tree .monaco-tree-row:hover:not(.highlighted) .foldermatch .monaco-icon-label, -.search-view .monaco-tree .monaco-tree-row.focused .foldermatch .monaco-icon-label, -.search-view .monaco-tree .monaco-tree-row:hover:not(.highlighted) .filematch .monaco-icon-label, -.search-view .monaco-tree .monaco-tree-row.focused .filematch .monaco-icon-label { +.search-view:not(.wide) .foldermatch .monaco-icon-label, +.search-view:not(.wide) .filematch .monaco-icon-label { + flex: 1; +} + +.search-view:not(.wide) .monaco-tree .monaco-tree-row:hover:not(.highlighted) .foldermatch .monaco-icon-label, +.search-view:not(.wide) .monaco-tree .monaco-tree-row.focused .foldermatch .monaco-icon-label, +.search-view:not(.wide) .monaco-tree .monaco-tree-row:hover:not(.highlighted) .filematch .monaco-icon-label, +.search-view:not(.wide) .monaco-tree .monaco-tree-row.focused .filematch .monaco-icon-label { flex: 1; } @@ -193,8 +198,8 @@ margin-left: 0.8em; } -.search-view .foldermatch .badge, -.search-view .filematch .badge { +.search-view.wide .foldermatch .badge, +.search-view.wide .filematch .badge { margin-left: 10px; } @@ -277,15 +282,24 @@ margin-right: 12px; } -.search-view > .results > .monaco-tree .monaco-tree-row:hover .content .filematch .monaco-count-badge, -.search-view > .results > .monaco-tree .monaco-tree-row:hover .content .foldermatch .monaco-count-badge, -.search-view > .results > .monaco-tree .monaco-tree-row:hover .content .linematch .monaco-count-badge, -.search-view > .results > .monaco-tree.focused .monaco-tree-row.focused .content .filematch .monaco-count-badge, -.search-view > .results > .monaco-tree.focused .monaco-tree-row.focused .content .foldermatch .monaco-count-badge, -.search-view > .results > .monaco-tree.focused .monaco-tree-row.focused .content .linematch .monaco-count-badge { +.search-view:not(.wide) > .results > .monaco-tree .monaco-tree-row:hover .content .filematch .monaco-count-badge, +.search-view:not(.wide) > .results > .monaco-tree .monaco-tree-row:hover .content .foldermatch .monaco-count-badge, +.search-view:not(.wide) > .results > .monaco-tree .monaco-tree-row:hover .content .linematch .monaco-count-badge, +.search-view:not(.wide) > .results > .monaco-tree .monaco-tree-row.focused .content .filematch .monaco-count-badge, +.search-view:not(.wide) > .results > .monaco-tree .monaco-tree-row.focused .content .foldermatch .monaco-count-badge, +.search-view:not(.wide) > .results > .monaco-tree .monaco-tree-row.focused .content .linematch .monaco-count-badge { display: none; } +.search-view.wide > .results > .monaco-tree .monaco-tree-row:hover .content .filematch .badge, +.search-view.wide > .results > .monaco-tree .monaco-tree-row:hover .content .foldermatch .badge, +.search-view.wide > .results > .monaco-tree .monaco-tree-row:hover .content .linematch .badge, +.search-view.wide > .results > .monaco-tree .monaco-tree-row.focused .content .filematch .badge, +.search-view.wide > .results > .monaco-tree .monaco-tree-row.focused .content .foldermatch .badge, +.search-view.wide > .results > .monaco-tree .monaco-tree-row.focused .content .linematch .badge { + flex: 1; +} + .search-view .focused .monaco-tree-row.selected:not(.highlighted) > .content.actions .action-remove, .vs-dark .monaco-workbench .search-view .focused .monaco-tree-row.selected:not(.highlighted) > .content.actions .action-remove { background: url("action-remove-focus.svg") center center no-repeat; diff --git a/src/vs/workbench/parts/search/browser/patternInputWidget.ts b/src/vs/workbench/parts/search/browser/patternInputWidget.ts index c83d533c470..2b900f9f3c9 100644 --- a/src/vs/workbench/parts/search/browser/patternInputWidget.ts +++ b/src/vs/workbench/parts/search/browser/patternInputWidget.ts @@ -5,7 +5,6 @@ import * as nls from 'vs/nls'; import * as dom from 'vs/base/browser/dom'; -import { $ } from 'vs/base/browser/builder'; import { Widget } from 'vs/base/browser/ui/widget'; import { Checkbox } from 'vs/base/browser/ui/checkbox/checkbox'; import { IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview'; @@ -73,7 +72,7 @@ export class PatternInputWidget extends Widget { switch (eventType) { case 'keydown': case 'keyup': - $(this.inputBox.inputElement).on(eventType, handler); + this._register(dom.addDisposableListener(this.inputBox.inputElement, eventType, handler)); break; case PatternInputWidget.OPTION_CHANGE: this.onOptionChange = handler; @@ -158,7 +157,7 @@ export class PatternInputWidget extends Widget { private render(): void { this.domNode = document.createElement('div'); this.domNode.style.width = this.width + 'px'; - $(this.domNode).addClass('monaco-findInput'); + dom.addClass(this.domNode, 'monaco-findInput'); this.inputBox = new InputBox(this.domNode, this.contextViewProvider, { placeholder: this.placeholder || '', diff --git a/src/vs/workbench/parts/search/browser/searchActions.ts b/src/vs/workbench/parts/search/browser/searchActions.ts index 8dcb979e257..af43ddcccf4 100644 --- a/src/vs/workbench/parts/search/browser/searchActions.ts +++ b/src/vs/workbench/parts/search/browser/searchActions.ts @@ -11,22 +11,27 @@ import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { ITree } from 'vs/base/parts/tree/browser/tree'; import { INavigator } from 'vs/base/common/iterator'; import { SearchView } from 'vs/workbench/parts/search/browser/searchView'; -import { Match, FileMatch, FileMatchOrMatch, FolderMatch, RenderableMatch } from 'vs/workbench/parts/search/common/searchModel'; +import { Match, FileMatch, FileMatchOrMatch, FolderMatch, RenderableMatch, SearchResult } from 'vs/workbench/parts/search/common/searchModel'; import { IReplaceService } from 'vs/workbench/parts/search/common/replace'; import * as Constants from 'vs/workbench/parts/search/common/constants'; import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; import { ResolvedKeybinding, createKeybinding } from 'vs/base/common/keyCodes'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { OS } from 'vs/base/common/platform'; +import { OS, isWindows } from 'vs/base/common/platform'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; import { VIEW_ID } from 'vs/platform/search/common/search'; +import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; +import { ICommandHandler } from 'vs/platform/commands/common/commands'; +import { Schemas } from 'vs/base/common/network'; +import { getPathLabel } from 'vs/base/common/labels'; +import URI from 'vs/base/common/uri'; export function isSearchViewFocused(viewletService: IViewletService, panelService: IPanelService): boolean { let searchView = getSearchView(viewletService, panelService); let activeElement = document.activeElement; - return searchView && activeElement && DOM.isAncestor(activeElement, searchView.getContainer().getHTMLElement()); + return searchView && activeElement && DOM.isAncestor(activeElement, searchView.getContainer()); } export function appendKeyBindingLabel(label: string, keyBinding: number | ResolvedKeybinding, keyBindingService2: IKeybindingService): string { @@ -121,6 +126,48 @@ export class ShowPreviousSearchIncludeAction extends Action { } } +export class ShowNextSearchExcludeAction extends Action { + + public static readonly ID = 'search.history.showNextExcludePattern'; + public static readonly LABEL = nls.localize('nextSearchExcludePattern', "Show Next Search Exclude Pattern"); + + constructor(id: string, label: string, + @IViewletService private viewletService: IViewletService, + @IPanelService private panelService: IPanelService, + @IContextKeyService private contextKeyService: IContextKeyService + ) { + super(id, label); + this.enabled = this.contextKeyService.contextMatchesRules(Constants.SearchViewVisibleKey); + } + + public run(): TPromise { + const searchView = getSearchView(this.viewletService, this.panelService); + searchView.searchExcludePattern.showNextTerm(); + return TPromise.as(null); + } +} + +export class ShowPreviousSearchExcludeAction extends Action { + + public static readonly ID = 'search.history.showPreviousExcludePattern'; + public static readonly LABEL = nls.localize('previousSearchExcludePattern', "Show Previous Search Exclude Pattern"); + + constructor(id: string, label: string, + @IViewletService private viewletService: IViewletService, + @IContextKeyService private contextKeyService: IContextKeyService, + @IPanelService private panelService: IPanelService + ) { + super(id, label); + this.enabled = this.contextKeyService.contextMatchesRules(Constants.SearchViewVisibleKey); + } + + public run(): TPromise { + const searchView = getSearchView(this.viewletService, this.panelService); + searchView.searchExcludePattern.showPreviousTerm(); + return TPromise.as(null); + } +} + export class ShowNextSearchTermAction extends Action { public static readonly ID = 'search.history.showNext'; @@ -216,21 +263,16 @@ export abstract class FindOrReplaceInFilesAction extends Action { } public run(): TPromise { - const searchView = getSearchView(this.viewletService, this.panelService); return openSearchView(this.viewletService, this.panelService, true).then(openedView => { - if (!searchView || this.expandSearchReplaceWidget) { - const searchAndReplaceWidget = openedView.searchAndReplaceWidget; - searchAndReplaceWidget.toggleReplace(this.expandSearchReplaceWidget); - // Focus replace only when there is text in the searchInput box - const focusReplace = this.focusReplace && searchAndReplaceWidget.searchInput.getValue(); - searchAndReplaceWidget.focus(this.selectWidgetText, !!focusReplace); - } + const searchAndReplaceWidget = openedView.searchAndReplaceWidget; + searchAndReplaceWidget.toggleReplace(this.expandSearchReplaceWidget); + // Focus replace only when there is text in the searchInput box + const focusReplace = this.focusReplace && searchAndReplaceWidget.searchInput.getValue(); + searchAndReplaceWidget.focus(this.selectWidgetText, !!focusReplace); }); } } -export const SHOW_SEARCH_LABEL = nls.localize('showSearchViewlet', "Show Search"); - export class FindInFilesAction extends FindOrReplaceInFilesAction { public static readonly LABEL = nls.localize('findInFiles', "Find in Files"); @@ -474,8 +516,10 @@ export abstract class AbstractSearchAndReplaceAction extends Action { export class RemoveAction extends AbstractSearchAndReplaceAction { + public static LABEL = nls.localize('RemoveAction.label', "Dismiss"); + constructor(private viewer: ITree, private element: RenderableMatch) { - super('remove', nls.localize('RemoveAction.label', "Dismiss"), 'action-remove'); + super('remove', RemoveAction.LABEL, 'action-remove'); } public run(): TPromise { @@ -508,9 +552,11 @@ export class RemoveAction extends AbstractSearchAndReplaceAction { export class ReplaceAllAction extends AbstractSearchAndReplaceAction { + public static readonly LABEL = nls.localize('file.replaceAll.label', "Replace All"); + constructor(private viewer: ITree, private fileMatch: FileMatch, private viewlet: SearchView, @IKeybindingService keyBindingService: IKeybindingService) { - super(Constants.ReplaceAllInFileActionId, appendKeyBindingLabel(nls.localize('file.replaceAll.label', "Replace All"), keyBindingService.lookupKeybinding(Constants.ReplaceAllInFileActionId), keyBindingService), 'action-replace-all'); + super(Constants.ReplaceAllInFileActionId, appendKeyBindingLabel(ReplaceAllAction.LABEL, keyBindingService.lookupKeybinding(Constants.ReplaceAllInFileActionId), keyBindingService), 'action-replace-all'); } public run(): TPromise { @@ -527,10 +573,12 @@ export class ReplaceAllAction extends AbstractSearchAndReplaceAction { export class ReplaceAllInFolderAction extends AbstractSearchAndReplaceAction { + public static readonly LABEL = nls.localize('file.replaceAll.label', "Replace All"); + constructor(private viewer: ITree, private folderMatch: FolderMatch, @IKeybindingService keyBindingService: IKeybindingService ) { - super(Constants.ReplaceAllInFolderActionId, appendKeyBindingLabel(nls.localize('file.replaceAll.label', "Replace All"), keyBindingService.lookupKeybinding(Constants.ReplaceAllInFolderActionId), keyBindingService), 'action-replace-all'); + super(Constants.ReplaceAllInFolderActionId, appendKeyBindingLabel(ReplaceAllInFolderAction.LABEL, keyBindingService.lookupKeybinding(Constants.ReplaceAllInFolderActionId), keyBindingService), 'action-replace-all'); } public async run(): TPromise { @@ -546,11 +594,13 @@ export class ReplaceAllInFolderAction extends AbstractSearchAndReplaceAction { export class ReplaceAction extends AbstractSearchAndReplaceAction { + public static readonly LABEL = nls.localize('match.replace.label', "Replace"); + constructor(private viewer: ITree, private element: Match, private viewlet: SearchView, @IReplaceService private replaceService: IReplaceService, @IKeybindingService keyBindingService: IKeybindingService, @IWorkbenchEditorService private editorService: IWorkbenchEditorService) { - super(Constants.ReplaceActionId, appendKeyBindingLabel(nls.localize('match.replace.label', "Replace"), keyBindingService.lookupKeybinding(Constants.ReplaceActionId), keyBindingService), 'action-replace'); + super(Constants.ReplaceActionId, appendKeyBindingLabel(ReplaceAction.LABEL, keyBindingService.lookupKeybinding(Constants.ReplaceActionId), keyBindingService), 'action-replace'); } public run(): TPromise { @@ -622,3 +672,92 @@ export class ReplaceAction extends AbstractSearchAndReplaceAction { return false; } } + +function uriToClipboardString(resource: URI): string { + return resource.scheme === Schemas.file ? getPathLabel(resource) : resource.toString(); +} + +export const copyPathCommand: ICommandHandler = (accessor, fileMatch: FileMatch | FolderMatch) => { + const clipboardService = accessor.get(IClipboardService); + + const text = uriToClipboardString(fileMatch.resource()); + clipboardService.writeText(text); +}; + +function matchToString(match: Match): string { + return `${match.range().startLineNumber},${match.range().startColumn}: ${match.text()}`; +} + +const lineDelimiter = isWindows ? '\r\n' : '\n'; +function fileMatchToString(fileMatch: FileMatch, maxMatches: number): { text: string, count: number } { + const matchTextRows = fileMatch.matches() + .slice(0, maxMatches) + .map(matchToString) + .map(matchText => ' ' + matchText); + + return { + text: `${uriToClipboardString(fileMatch.resource())}${lineDelimiter}${matchTextRows.join(lineDelimiter)}`, + count: matchTextRows.length + }; +} + +function folderMatchToString(folderMatch: FolderMatch, maxMatches: number): { text: string, count: number } { + const fileResults: string[] = []; + let numMatches = 0; + + for (let i = 0; i < folderMatch.fileCount() && numMatches < maxMatches; i++) { + const fileResult = fileMatchToString(folderMatch.matches()[i], maxMatches - numMatches); + numMatches += fileResult.count; + fileResults.push(fileResult.text); + } + + return { + text: fileResults.join(lineDelimiter + lineDelimiter), + count: numMatches + }; +} + +const maxClipboardMatches = 1e4; +export const copyMatchCommand: ICommandHandler = (accessor, match: RenderableMatch) => { + const clipboardService = accessor.get(IClipboardService); + + let text: string; + if (match instanceof Match) { + text = matchToString(match); + } else if (match instanceof FileMatch) { + text = fileMatchToString(match, maxClipboardMatches).text; + } else if (match instanceof FolderMatch) { + text = folderMatchToString(match, maxClipboardMatches).text; + } + + if (text) { + clipboardService.writeText(text); + } +}; + +function allFolderMatchesToString(folderMatches: FolderMatch[], maxMatches: number): string { + const folderResults: string[] = []; + let numMatches = 0; + + for (let i = 0; i < folderMatches.length && numMatches < maxMatches; i++) { + const folderResult = folderMatchToString(folderMatches[i], maxMatches - numMatches); + if (folderResult.count) { + numMatches += folderResult.count; + folderResults.push(folderResult.text); + } + } + + return folderResults.join(lineDelimiter + lineDelimiter); +} + +export const copyAllCommand: ICommandHandler = (accessor) => { + const viewletService = accessor.get(IViewletService); + const panelService = accessor.get(IPanelService); + const clipboardService = accessor.get(IClipboardService); + + const searchView = getSearchView(viewletService, panelService); + const root: SearchResult = searchView.getControl().getInput(); + + const text = allFolderMatchesToString(root.folderMatches(), maxClipboardMatches); + clipboardService.writeText(text); +}; diff --git a/src/vs/workbench/parts/search/browser/searchResultsView.ts b/src/vs/workbench/parts/search/browser/searchResultsView.ts index 188f2c1c147..4515d7d943d 100644 --- a/src/vs/workbench/parts/search/browser/searchResultsView.ts +++ b/src/vs/workbench/parts/search/browser/searchResultsView.ts @@ -12,7 +12,7 @@ import { IAction, IActionRunner } from 'vs/base/common/actions'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge'; import { FileLabel } from 'vs/workbench/browser/labels'; -import { ITree, IDataSource, ISorter, IAccessibilityProvider, IFilter, IRenderer } from 'vs/base/parts/tree/browser/tree'; +import { ITree, IDataSource, ISorter, IAccessibilityProvider, IFilter, IRenderer, ContextMenuEvent } from 'vs/base/parts/tree/browser/tree'; import { Match, SearchResult, FileMatch, FileMatchOrMatch, SearchModel, FolderMatch } from 'vs/workbench/parts/search/common/searchModel'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { Range } from 'vs/editor/common/core/range'; @@ -23,6 +23,11 @@ import { attachBadgeStyler } from 'vs/platform/theme/common/styler'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { getPathLabel } from 'vs/base/common/labels'; import { FileKind } from 'vs/platform/files/common/files'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IMenuService, MenuId, IMenu } from 'vs/platform/actions/common/actions'; +import { WorkbenchTreeController, WorkbenchTree } from 'vs/platform/list/browser/listService'; +import { fillInActions } from 'vs/platform/actions/browser/menuItemActionItem'; export class SearchDataSource implements IDataSource { @@ -339,10 +344,12 @@ export class SearchAccessibilityProvider implements IAccessibilityProvider { const replace = searchModel.isReplaceActive() && !!searchModel.replaceString; const matchString = match.getMatchString(); const range = match.range(); + const matchText = match.text().substr(0, range.endColumn + 150); if (replace) { - return nls.localize('replacePreviewResultAria', "Replace term {0} with {1} at column position {2} in line with text {3}", matchString, match.replaceString, range.startColumn + 1, match.text()); + return nls.localize('replacePreviewResultAria', "Replace term {0} with {1} at column position {2} in line with text {3}", matchString, match.replaceString, range.startColumn + 1, matchText); } - return nls.localize('searchResultAria', "Found term {0} at column position {1} in line with text {2}", matchString, range.startColumn + 1, match.text()); + + return nls.localize('searchResultAria', "Found term {0} at column position {1} in line with text {2}", matchString, range.startColumn + 1, matchText); } return undefined; } @@ -354,3 +361,38 @@ export class SearchFilter implements IFilter { return !(element instanceof FileMatch || element instanceof FolderMatch) || element.matches().length > 0; } } + +export class SearchTreeController extends WorkbenchTreeController { + private contextMenu: IMenu; + + constructor( + @IContextMenuService private contextMenuService: IContextMenuService, + @IMenuService private menuService: IMenuService, + @IConfigurationService configurationService: IConfigurationService + ) { + super({}, configurationService); + } + + public onContextMenu(tree: WorkbenchTree, element: any, event: ContextMenuEvent): boolean { + if (!this.contextMenu) { + this.contextMenu = this.menuService.createMenu(MenuId.SearchContext, tree.contextKeyService); + } + + tree.setFocus(element); + + const anchor = { x: event.posx, y: event.posy }; + this.contextMenuService.showContextMenu({ + getAnchor: () => anchor, + + getActions: () => { + const actions: IAction[] = []; + fillInActions(this.contextMenu, { shouldForwardArgs: true }, actions, this.contextMenuService); + return TPromise.as(actions); + }, + + getActionsContext: () => element + }); + + return true; + } +} diff --git a/src/vs/workbench/parts/search/browser/searchView.ts b/src/vs/workbench/parts/search/browser/searchView.ts index f6b01d6a0f2..f367e4aae41 100644 --- a/src/vs/workbench/parts/search/browser/searchView.ts +++ b/src/vs/workbench/parts/search/browser/searchView.ts @@ -19,11 +19,11 @@ import * as paths from 'vs/base/common/paths'; import * as dom from 'vs/base/browser/dom'; import { IAction } from 'vs/base/common/actions'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; -import { Dimension, Builder, $ } from 'vs/base/browser/builder'; +import { Builder, $ } from 'vs/base/browser/builder'; import { FindInput } from 'vs/base/browser/ui/findinput/findInput'; import { ITree, IFocusEvent } from 'vs/base/parts/tree/browser/tree'; import { Scope } from 'vs/workbench/common/memento'; -import { IPreferencesService } from 'vs/workbench/parts/preferences/common/preferences'; +import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService'; import { FileChangeType, FileChangesEvent, IFileService } from 'vs/platform/files/common/files'; import { Match, FileMatch, SearchModel, FileMatchOrMatch, IChangeEvent, ISearchWorkbenchService, FolderMatch } from 'vs/workbench/parts/search/common/searchModel'; @@ -41,7 +41,7 @@ import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/c import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { KeyCode } from 'vs/base/common/keyCodes'; import { PatternInputWidget, ExcludePatternInputWidget } from 'vs/workbench/parts/search/browser/patternInputWidget'; -import { SearchRenderer, SearchDataSource, SearchSorter, SearchAccessibilityProvider, SearchFilter } from 'vs/workbench/parts/search/browser/searchResultsView'; +import { SearchRenderer, SearchDataSource, SearchSorter, SearchAccessibilityProvider, SearchFilter, SearchTreeController } from 'vs/workbench/parts/search/browser/searchResultsView'; import { SearchWidget, ISearchWidgetOptions } from 'vs/workbench/parts/search/browser/searchWidget'; import { RefreshAction, CollapseDeepestExpandedLevelAction, ClearSearchResultsAction, CancelSearchAction } from 'vs/workbench/parts/search/browser/searchActions'; import { IReplaceService } from 'vs/workbench/parts/search/common/replace'; @@ -62,13 +62,15 @@ import { IPanel } from 'vs/workbench/common/panel'; import { IViewlet } from 'vs/workbench/common/viewlet'; import { Viewlet } from 'vs/workbench/browser/viewlet'; import { IPartService } from 'vs/workbench/services/part/common/partService'; -import { splitGlobAware } from 'vs/base/common/glob'; export class SearchView extends Viewlet implements IViewlet, IPanel { private static readonly MAX_TEXT_RESULTS = 10000; private static readonly SHOW_REPLACE_STORAGE_KEY = 'vs.search.show.replace'; + private static readonly WIDE_CLASS_NAME = 'wide'; + private static readonly WIDE_VIEW_SIZE = 600; + private isDisposed: boolean; private queryBuilder: QueryBuilder; @@ -77,12 +79,15 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { private viewletVisible: IContextKey; private inputBoxFocused: IContextKey; private inputPatternIncludesFocused: IContextKey; + private inputPatternExclusionsFocused: IContextKey; private firstMatchFocused: IContextKey; private fileMatchOrMatchFocused: IContextKey; + private fileMatchOrFolderMatchFocus: IContextKey; private fileMatchFocused: IContextKey; private folderMatchFocused: IContextKey; private matchFocused: IContextKey; private hasSearchResultsKey: IContextKey; + private searchSubmitted: boolean; private searching: boolean; @@ -92,9 +97,10 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { private messages: Builder; private searchWidgetsContainer: Builder; private searchWidget: SearchWidget; - private size: Dimension; + private size: dom.Dimension; private queryDetails: HTMLElement; - private inputPatternIncludes: ExcludePatternInputWidget; + private inputPatternExcludes: ExcludePatternInputWidget; + private inputPatternIncludes: PatternInputWidget; private results: Builder; private currentSelectedFileMatch: FileMatch; @@ -131,8 +137,10 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { this.viewletVisible = Constants.SearchViewVisibleKey.bindTo(contextKeyService); this.inputBoxFocused = Constants.InputBoxFocusedKey.bindTo(this.contextKeyService); this.inputPatternIncludesFocused = Constants.PatternIncludesFocusedKey.bindTo(this.contextKeyService); + this.inputPatternExclusionsFocused = Constants.PatternExcludesFocusedKey.bindTo(this.contextKeyService); this.firstMatchFocused = Constants.FirstMatchFocusKey.bindTo(contextKeyService); this.fileMatchOrMatchFocused = Constants.FileMatchOrMatchFocusKey.bindTo(contextKeyService); + this.fileMatchOrFolderMatchFocus = Constants.FileMatchOrFolderMatchFocusKey.bindTo(contextKeyService); this.fileMatchFocused = Constants.FileFocusKey.bindTo(contextKeyService); this.folderMatchFocused = Constants.FolderFocusKey.bindTo(contextKeyService); this.matchFocused = Constants.MatchFocusKey.bindTo(this.contextKeyService); @@ -158,12 +166,12 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { } } - public create(parent: Builder): TPromise { + public create(parent: HTMLElement): TPromise { super.create(parent); this.viewModel = this.searchWorkbenchService.searchModel; let builder: Builder; - parent.div({ + $(parent).div({ 'class': 'search-view' }, (div) => { builder = div; @@ -175,14 +183,43 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { this.createSearchWidget(this.searchWidgetsContainer); const filePatterns = this.viewletSettings['query.filePatterns'] || ''; - const patternExclusions = this.viewletSettings['query.folderExclusions'] || ''; - delete this.viewletSettings['query.folderExclusions']; // Migrating from versions that have dedicated exclusions persisted - const patternIncludes = this.viewletSettings['query.folderIncludes'] || ''; - const patternIncludesHistory = this.viewletSettings['query.folderIncludesHistory'] || []; + let patternExclusions = this.viewletSettings['query.folderExclusions'] || ''; + const patternExclusionsHistory: string[] = this.viewletSettings['query.folderExclusionsHistory'] || []; + let patternIncludes = this.viewletSettings['query.folderIncludes'] || ''; + let patternIncludesHistory: string[] = this.viewletSettings['query.folderIncludesHistory'] || []; const queryDetailsExpanded = this.viewletSettings['query.queryDetailsExpanded'] || ''; const useExcludesAndIgnoreFiles = typeof this.viewletSettings['query.useExcludesAndIgnoreFiles'] === 'boolean' ? this.viewletSettings['query.useExcludesAndIgnoreFiles'] : true; + // Transition history from 1.22 combined include+exclude, to split include/exclude histories + const patternIncludesHistoryWithoutExcludes: string[] = []; + const patternExcludesHistoryFromIncludes: string[] = []; + patternIncludesHistory.forEach(historyEntry => { + const includeExclude = this.queryBuilder.parseIncludeExcludePattern(historyEntry); + if (includeExclude.includePattern) { + patternIncludesHistoryWithoutExcludes.push(includeExclude.includePattern); + } + + if (includeExclude.excludePattern) { + patternExcludesHistoryFromIncludes.push(includeExclude.excludePattern); + } + }); + + patternIncludesHistory = patternIncludesHistoryWithoutExcludes; + patternExclusionsHistory.push(...patternExcludesHistoryFromIncludes); + + // Split combined include/exclude to split include/exclude boxes + const includeExclude = this.queryBuilder.parseIncludeExcludePattern(patternIncludes); + patternIncludes = includeExclude.includePattern || ''; + + if (includeExclude.excludePattern) { + if (patternExclusions) { + patternExclusions += ', ' + includeExclude.excludePattern; + } else { + patternExclusions = includeExclude.excludePattern; + } + } + this.queryDetails = this.searchWidgetsContainer.div({ 'class': ['query-details'] }, (builder) => { builder.div({ 'class': 'more', 'tabindex': 0, 'role': 'button', 'title': nls.localize('moreSearch', "Toggle Search Details") }) .on(dom.EventType.CLICK, (e) => { @@ -198,32 +235,16 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { }); //folder includes list - builder.div({ 'class': 'file-types' }, (builder) => { - let title = nls.localize('searchIncludeExclude.label', "files to include/exclude"); + builder.div({ 'class': 'file-types includes' }, (builder) => { + let title = nls.localize('searchScope.includes', "files to include"); builder.element('h4', { text: title }); - this.inputPatternIncludes = new ExcludePatternInputWidget(builder.getContainer(), this.contextViewService, this.themeService, { - ariaLabel: nls.localize('searchIncludeExclude.ariaLabel', 'Search Include/Exclude Patterns'), - placeholder: nls.localize('searchIncludeExclude.placeholder', "Examples: src, !*.ts, test/**/*.log") + this.inputPatternIncludes = new PatternInputWidget(builder.getContainer(), this.contextViewService, this.themeService, { + ariaLabel: nls.localize('label.includes', 'Search Include Patterns') }); - // For migrating from versions that have dedicated exclusions persisted - let mergedIncludeExcludes = patternIncludes; - if (patternExclusions) { - if (mergedIncludeExcludes) { - mergedIncludeExcludes += ', '; - } - - mergedIncludeExcludes += splitGlobAware(patternExclusions, ',') - .map(s => s.trim()) - .filter(s => !!s.length) - .map(s => '!' + s) - .join(', '); - } - - this.inputPatternIncludes.setValue(mergedIncludeExcludes); + this.inputPatternIncludes.setValue(patternIncludes); this.inputPatternIncludes.setHistory(patternIncludesHistory); - this.inputPatternIncludes.setUseExcludesAndIgnoreFiles(useExcludesAndIgnoreFiles); this.inputPatternIncludes .on(FindInput.OPTION_CHANGE, (e) => { @@ -234,6 +255,30 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { this.inputPatternIncludes.onCancel(() => this.viewModel.cancelSearch()); // Cancel search without focusing the search widget this.trackInputBox(this.inputPatternIncludes.inputFocusTracker, this.inputPatternIncludesFocused); }); + + //pattern exclusion list + builder.div({ 'class': 'file-types excludes' }, (builder) => { + let title = nls.localize('searchScope.excludes', "files to exclude"); + builder.element('h4', { text: title }); + + this.inputPatternExcludes = new ExcludePatternInputWidget(builder.getContainer(), this.contextViewService, this.themeService, { + ariaLabel: nls.localize('label.excludes', 'Search Exclude Patterns') + }); + + this.inputPatternExcludes.setValue(patternExclusions); + this.inputPatternExcludes.setUseExcludesAndIgnoreFiles(useExcludesAndIgnoreFiles); + this.inputPatternExcludes.setHistory(patternExclusionsHistory); + + this.inputPatternExcludes + .on(FindInput.OPTION_CHANGE, (e) => { + this.onQueryChanged(false); + }); + + this.inputPatternExcludes.onSubmit(() => this.onQueryChanged(true, true)); + this.inputPatternExcludes.onSubmit(() => this.onQueryChanged(true, true)); + this.inputPatternExcludes.onCancel(() => this.viewModel.cancelSearch()); // Cancel search without focusing the search widget + this.trackInputBox(this.inputPatternExcludes.inputFocusTracker, this.inputPatternExclusionsFocused); + }); }).getHTMLElement(); this.messages = builder.div({ 'class': 'messages' }).hide().clone(); @@ -266,6 +311,10 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { return this.inputPatternIncludes; } + public get searchExcludePattern(): PatternInputWidget { + return this.inputPatternExcludes; + } + private updateActions(): void { for (const action of this.actions) { action.update(); @@ -322,7 +371,8 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { this.toUnbind.push(inputFocusTracker.onDidBlur(() => { this.inputBoxFocused.set(this.searchWidget.searchInputHasFocus() || this.searchWidget.replaceInputHasFocus() - || this.inputPatternIncludes.inputHasFocus()); + || this.inputPatternIncludes.inputHasFocus() + || this.inputPatternExcludes.inputHasFocus()); if (contextKey) { contextKey.set(false); } @@ -493,6 +543,7 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { renderer: renderer, sorter: new SearchSorter(), filter: new SearchFilter(), + controller: this.instantiationService.createInstance(SearchTreeController), accessibilityProvider: this.instantiationService.createInstance(SearchAccessibilityProvider), dnd }, { @@ -526,10 +577,11 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { if (treeHasFocus) { const focus = e.focus; this.firstMatchFocused.set(this.tree.getNavigator().first() === focus); - this.fileMatchOrMatchFocused.set(true); + this.fileMatchOrMatchFocused.set(!!focus); this.fileMatchFocused.set(focus instanceof FileMatch); this.folderMatchFocused.set(focus instanceof FolderMatch); this.matchFocused.set(focus instanceof Match); + this.fileMatchOrFolderMatchFocus.set(focus instanceof FileMatch || focus instanceof FolderMatch); } })); @@ -540,9 +592,8 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { this.fileMatchFocused.reset(); this.folderMatchFocused.reset(); this.matchFocused.reset(); + this.fileMatchOrFolderMatchFocus.reset(); })); - - }); } @@ -699,6 +750,12 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { } if (this.inputPatternIncludes.inputHasFocus()) { + this.inputPatternExcludes.focus(); + this.inputPatternExcludes.select(); + return; + } + + if (this.inputPatternExcludes.inputHasFocus()) { this.selectTreeIfNotSelected(); return; } @@ -727,6 +784,12 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { return; } + if (this.inputPatternExcludes.inputHasFocus()) { + this.inputPatternIncludes.focus(); + this.inputPatternIncludes.select(); + return; + } + if (this.tree.isDOMFocused()) { this.moveFocusFromResults(); return; @@ -735,7 +798,7 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { private moveFocusFromResults(): void { if (this.showsFileTypes()) { - this.toggleQueryDetails(true, true, false); + this.toggleQueryDetails(true, true, false, true); } else { this.searchWidget.focus(true, true); } @@ -746,8 +809,15 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { return; } + if (this.size.width >= SearchView.WIDE_VIEW_SIZE) { + dom.addClass(this.getContainer(), SearchView.WIDE_CLASS_NAME); + } else { + dom.removeClass(this.getContainer(), SearchView.WIDE_CLASS_NAME); + } + this.searchWidget.setWidth(this.size.width - 28 /* container margin */); + this.inputPatternExcludes.setWidth(this.size.width - 28 /* container margin */); this.inputPatternIncludes.setWidth(this.size.width - 28 /* container margin */); const messagesSize = this.messages.isHidden() ? 0 : dom.getTotalHeight(this.messages.getHTMLElement()); @@ -760,7 +830,7 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { this.tree.layout(searchResultContainerSize); } - public layout(dimension: Dimension): void { + public layout(dimension: dom.Dimension): void { this.size = dimension; this.reLayout(); } @@ -868,7 +938,7 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { this.onQueryChanged(true, true); } - public toggleQueryDetails(moveFocus?: boolean, show?: boolean, skipLayout?: boolean): void { + public toggleQueryDetails(moveFocus?: boolean, show?: boolean, skipLayout?: boolean, reverse?: boolean): void { let cls = 'more'; show = typeof show === 'undefined' ? !dom.hasClass(this.queryDetails, cls) : Boolean(show); this.viewletSettings['query.queryDetailsExpanded'] = show; @@ -877,8 +947,13 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { if (show) { dom.addClass(this.queryDetails, cls); if (moveFocus) { - this.inputPatternIncludes.focus(); - this.inputPatternIncludes.select(); + if (reverse) { + this.inputPatternExcludes.focus(); + this.inputPatternExcludes.select(); + } else { + this.inputPatternIncludes.focus(); + this.inputPatternIncludes.select(); + } } } else { dom.removeClass(this.queryDetails, cls); @@ -946,7 +1021,9 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { const isWholeWords = this.searchWidget.searchInput.getWholeWords(); const isCaseSensitive = this.searchWidget.searchInput.getCaseSensitive(); const contentPattern = this.searchWidget.searchInput.getValue(); - const useExcludesAndIgnoreFiles = this.inputPatternIncludes.useExcludesAndIgnoreFiles(); + const excludePatternText = this.inputPatternExcludes.getValue().trim(); + const includePatternText = this.inputPatternIncludes.getValue().trim(); + const useExcludesAndIgnoreFiles = this.inputPatternExcludes.useExcludesAndIgnoreFiles(); if (!rerunQuery) { return; @@ -979,8 +1056,8 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { isSmartCase: this.configurationService.getValue().search.smartCase }; - const includeExcludePattern = this.inputPatternIncludes.getValue().trim(); - const { includePattern, excludePattern } = this.queryBuilder.parseIncludeExcludePattern(includeExcludePattern); + const excludePattern = this.inputPatternExcludes.getValue(); + const includePattern = this.inputPatternIncludes.getValue(); const options: IQueryOptions = { extraFileResources: getOutOfWorkspaceEditorResources(this.editorGroupService, this.contextService), @@ -1006,7 +1083,7 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { } this.validateQuery(query).then(() => { - this.onQueryTriggered(query, excludePattern, includePattern); + this.onQueryTriggered(query, excludePatternText, includePatternText); if (!preserveFocus) { this.searchWidget.focus(false); // focus back to input field @@ -1037,6 +1114,7 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { } private onQueryTriggered(query: ISearchQuery, excludePatternText: string, includePatternText: string): void { + this.inputPatternExcludes.onSearchSubmit(); this.inputPatternIncludes.onSearchSubmit(); this.viewModel.cancelSearch(); @@ -1097,8 +1175,8 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { } if (!hasResults) { - let hasExcludes = !!query.excludePattern; - let hasIncludes = !!query.includePattern; + let hasExcludes = !!excludePatternText; + let hasIncludes = !!includePatternText; let message: string; if (!completed) { @@ -1138,6 +1216,7 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { }).on(dom.EventType.CLICK, (e: MouseEvent) => { dom.EventHelper.stop(e, false); + this.inputPatternExcludes.setValue(''); this.inputPatternIncludes.setValue(''); this.onQueryChanged(true); @@ -1426,9 +1505,11 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { const isWholeWords = this.searchWidget.searchInput.getWholeWords(); const isCaseSensitive = this.searchWidget.searchInput.getCaseSensitive(); const contentPattern = this.searchWidget.searchInput.getValue(); + const patternExcludes = this.inputPatternExcludes.getValue().trim(); const patternIncludes = this.inputPatternIncludes.getValue().trim(); - const useExcludesAndIgnoreFiles = this.inputPatternIncludes.useExcludesAndIgnoreFiles(); + const useExcludesAndIgnoreFiles = this.inputPatternExcludes.useExcludesAndIgnoreFiles(); const searchHistory = this.searchWidget.getHistory(); + const patternExcludesHistory = this.inputPatternExcludes.getHistory(); const patternIncludesHistory = this.inputPatternIncludes.getHistory(); // store memento @@ -1437,7 +1518,9 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { this.viewletSettings['query.regex'] = isRegex; this.viewletSettings['query.wholeWords'] = isWholeWords; this.viewletSettings['query.caseSensitive'] = isCaseSensitive; + this.viewletSettings['query.folderExclusions'] = patternExcludes; this.viewletSettings['query.folderIncludes'] = patternIncludes; + this.viewletSettings['query.folderExclusionsHistory'] = patternExcludesHistory; this.viewletSettings['query.folderIncludesHistory'] = patternIncludesHistory; this.viewletSettings['query.useExcludesAndIgnoreFiles'] = useExcludesAndIgnoreFiles; @@ -1453,6 +1536,7 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { this.searchWidget.dispose(); this.inputPatternIncludes.dispose(); + this.inputPatternExcludes.dispose(); this.viewModel.dispose(); @@ -1478,16 +1562,16 @@ registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => { const diffInsertedOutlineColor = theme.getColor(diffInsertedOutline); if (diffInsertedOutlineColor) { - collector.addRule(`.monaco-workbench .search-view .replaceMatch:not(:empty) { border: 1px dashed ${diffInsertedOutlineColor}; }`); + collector.addRule(`.monaco-workbench .search-view .replaceMatch:not(:empty) { border: 1px ${theme.type === 'hc' ? 'dashed' : 'solid'} ${diffInsertedOutlineColor}; }`); } const diffRemovedOutlineColor = theme.getColor(diffRemovedOutline); if (diffRemovedOutlineColor) { - collector.addRule(`.monaco-workbench .search-view .replace.findInFileMatch { border: 1px dashed ${diffRemovedOutlineColor}; }`); + collector.addRule(`.monaco-workbench .search-view .replace.findInFileMatch { border: 1px ${theme.type === 'hc' ? 'dashed' : 'solid'} ${diffRemovedOutlineColor}; }`); } const findMatchHighlightBorder = theme.getColor(editorFindMatchHighlightBorder); if (findMatchHighlightBorder) { - collector.addRule(`.monaco-workbench .search-view .findInFileMatch { border: 1px dashed ${findMatchHighlightBorder}; }`); + collector.addRule(`.monaco-workbench .search-view .findInFileMatch { border: 1px ${theme.type === 'hc' ? 'dashed' : 'solid'} ${findMatchHighlightBorder}; }`); } }); diff --git a/src/vs/workbench/parts/search/browser/searchViewLocationUpdater.ts b/src/vs/workbench/parts/search/browser/searchViewLocationUpdater.ts index a7bc8a9babd..851e30dc4e5 100644 --- a/src/vs/workbench/parts/search/browser/searchViewLocationUpdater.ts +++ b/src/vs/workbench/parts/search/browser/searchViewLocationUpdater.ts @@ -30,7 +30,6 @@ export class SearchViewLocationUpdater implements IWorkbenchContribution { configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration('search.location')) { updateSearchViewLocation(); - } }); diff --git a/src/vs/workbench/parts/search/common/constants.ts b/src/vs/workbench/parts/search/common/constants.ts index 1c9a877bd20..b8ca4a9aa84 100644 --- a/src/vs/workbench/parts/search/common/constants.ts +++ b/src/vs/workbench/parts/search/common/constants.ts @@ -12,6 +12,9 @@ export const FocusSearchFromResults = 'search.action.focusSearchFromResults'; export const OpenMatchToSide = 'search.action.openResultToSide'; export const CancelActionId = 'search.action.cancel'; export const RemoveActionId = 'search.action.remove'; +export const CopyPathCommandId = 'search.action.copyPath'; +export const CopyMatchCommandId = 'search.action.copyMatch'; +export const CopyAllCommandId = 'search.action.copyAll'; export const ReplaceActionId = 'search.action.replace'; export const ReplaceAllInFileActionId = 'search.action.replaceAllInFile'; export const ReplaceAllInFolderActionId = 'search.action.replaceAllInFolder'; @@ -20,6 +23,8 @@ export const ToggleCaseSensitiveCommandId = 'toggleSearchCaseSensitive'; export const ToggleWholeWordCommandId = 'toggleSearchWholeWord'; export const ToggleRegexCommandId = 'toggleSearchRegex'; +export const ToggleSearchViewPositionCommandId = 'search.action.toggleSearchViewPosition'; + export const SearchViewVisibleKey = new RawContextKey('searchViewletVisible', true); export const InputBoxFocusedKey = new RawContextKey('inputBoxFocus', false); export const SearchInputBoxFocusedKey = new RawContextKey('searchInputBoxFocus', false); @@ -30,7 +35,8 @@ export const ReplaceActiveKey = new RawContextKey('replaceActive', fals export const HasSearchResults = new RawContextKey('hasSearchResult', false); export const FirstMatchFocusKey = new RawContextKey('firstMatchFocus', false); -export const FileMatchOrMatchFocusKey = new RawContextKey('fileMatchOrMatchFocus', false); +export const FileMatchOrMatchFocusKey = new RawContextKey('fileMatchOrMatchFocus', false); // This is actually, Match or File or Folder +export const FileMatchOrFolderMatchFocusKey = new RawContextKey('fileMatchOrFolderMatchFocus', false); export const FileFocusKey = new RawContextKey('fileMatchFocus', false); export const FolderFocusKey = new RawContextKey('folderMatchFocus', false); export const MatchFocusKey = new RawContextKey('matchFocus', false); diff --git a/src/vs/workbench/parts/search/common/searchModel.ts b/src/vs/workbench/parts/search/common/searchModel.ts index ac9827a3d67..6084c48c45d 100644 --- a/src/vs/workbench/parts/search/common/searchModel.ts +++ b/src/vs/workbench/parts/search/common/searchModel.ts @@ -95,6 +95,7 @@ export class FileMatch extends Disposable { private static readonly _CURRENT_FIND_MATCH = ModelDecorationOptions.register({ stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, + zIndex: 13, className: 'currentFindMatch', overviewRuler: { color: themeColorFromId(overviewRulerFindMatchForeground), diff --git a/src/vs/workbench/parts/search/electron-browser/search.contribution.ts b/src/vs/workbench/parts/search/electron-browser/search.contribution.ts index e38d72e2114..b575b602528 100644 --- a/src/vs/workbench/parts/search/electron-browser/search.contribution.ts +++ b/src/vs/workbench/parts/search/electron-browser/search.contribution.ts @@ -16,7 +16,7 @@ import { Action } from 'vs/base/common/actions'; import * as objects from 'vs/base/common/objects'; import * as platform from 'vs/base/common/platform'; import { ExplorerFolderContext, ExplorerRootContext } from 'vs/workbench/parts/files/common/files'; -import { SyncActionDescriptor, MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; +import { SyncActionDescriptor, MenuRegistry, MenuId, ICommandAction } from 'vs/platform/actions/common/actions'; import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions'; import { QuickOpenHandlerDescriptor, IQuickOpenRegistry, Extensions as QuickOpenExtensions } from 'vs/workbench/browser/quickopen'; import { KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; @@ -53,16 +53,19 @@ import { getMultiSelectedResources } from 'vs/workbench/parts/files/browser/file import { Schemas } from 'vs/base/common/network'; import { PanelRegistry, Extensions as PanelExtensions, PanelDescriptor } from 'vs/workbench/browser/panel'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; -import { openSearchView, getSearchView, ReplaceAllInFolderAction, ReplaceAllAction, CloseReplaceAction, FocusNextInputAction, FocusPreviousInputAction, FocusNextSearchResultAction, FocusPreviousSearchResultAction, ReplaceInFilesAction, FindInFilesAction, FocusActiveEditorCommand, toggleCaseSensitiveCommand, ShowNextSearchTermAction, ShowPreviousSearchTermAction, toggleRegexCommand, ShowPreviousSearchIncludeAction, ShowNextSearchIncludeAction, CollapseDeepestExpandedLevelAction, toggleWholeWordCommand, RemoveAction, ReplaceAction, ClearSearchResultsAction } from 'vs/workbench/parts/search/browser/searchActions'; -import { VIEW_ID } from 'vs/platform/search/common/search'; +import { openSearchView, getSearchView, ReplaceAllInFolderAction, ReplaceAllAction, CloseReplaceAction, FocusNextInputAction, FocusPreviousInputAction, FocusNextSearchResultAction, FocusPreviousSearchResultAction, ReplaceInFilesAction, FindInFilesAction, FocusActiveEditorCommand, toggleCaseSensitiveCommand, ShowNextSearchTermAction, ShowPreviousSearchTermAction, toggleRegexCommand, ShowPreviousSearchIncludeAction, ShowNextSearchIncludeAction, CollapseDeepestExpandedLevelAction, toggleWholeWordCommand, RemoveAction, ReplaceAction, ClearSearchResultsAction, copyPathCommand, copyMatchCommand, copyAllCommand, ShowNextSearchExcludeAction, ShowPreviousSearchExcludeAction } from 'vs/workbench/parts/search/browser/searchActions'; +import { VIEW_ID, ISearchConfigurationProperties } from 'vs/platform/search/common/search'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { SearchViewLocationUpdater } from 'vs/workbench/parts/search/browser/searchViewLocationUpdater'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; registerSingleton(ISearchWorkbenchService, SearchWorkbenchService); replaceContributions(); searchWidgetContributions(); +const category = nls.localize('search', "Search"); + KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'workbench.action.search.toggleQueryDetails', weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), @@ -130,8 +133,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ id: Constants.ReplaceActionId, weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.ReplaceActiveKey, Constants.MatchFocusKey), - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Enter, - secondary: [KeyMod.Shift | KeyMod.CtrlCmd | KeyCode.KEY_1], + primary: KeyMod.Shift | KeyMod.CtrlCmd | KeyCode.KEY_1, handler: (accessor, args: any) => { const searchView = getSearchView(accessor.get(IViewletService), accessor.get(IPanelService)); const tree: ITree = searchView.getControl(); @@ -143,7 +145,8 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ id: Constants.ReplaceAllInFileActionId, weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.ReplaceActiveKey, Constants.FileFocusKey), - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Enter, + primary: KeyMod.Shift | KeyMod.CtrlCmd | KeyCode.KEY_1, + secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Enter], handler: (accessor, args: any) => { const searchView = getSearchView(accessor.get(IViewletService), accessor.get(IPanelService)); const tree: ITree = searchView.getControl(); @@ -155,7 +158,8 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ id: Constants.ReplaceAllInFolderActionId, weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.ReplaceActiveKey, Constants.FolderFocusKey), - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Enter, + primary: KeyMod.Shift | KeyMod.CtrlCmd | KeyCode.KEY_1, + secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Enter], handler: (accessor, args: any) => { const searchView = getSearchView(accessor.get(IViewletService), accessor.get(IPanelService)); const tree: ITree = searchView.getControl(); @@ -193,6 +197,125 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ } }); +MenuRegistry.appendMenuItem(MenuId.SearchContext, { + command: { + id: Constants.ReplaceActionId, + title: ReplaceAction.LABEL + }, + when: ContextKeyExpr.and(Constants.ReplaceActiveKey, Constants.MatchFocusKey), + group: 'search', + order: 1 +}); + +MenuRegistry.appendMenuItem(MenuId.SearchContext, { + command: { + id: Constants.ReplaceAllInFolderActionId, + title: ReplaceAllInFolderAction.LABEL + }, + when: ContextKeyExpr.and(Constants.ReplaceActiveKey, Constants.FolderFocusKey), + group: 'search', + order: 1 +}); + +MenuRegistry.appendMenuItem(MenuId.SearchContext, { + command: { + id: Constants.ReplaceAllInFileActionId, + title: ReplaceAllAction.LABEL + }, + when: ContextKeyExpr.and(Constants.ReplaceActiveKey, Constants.FileFocusKey), + group: 'search', + order: 1 +}); + +MenuRegistry.appendMenuItem(MenuId.SearchContext, { + command: { + id: Constants.RemoveActionId, + title: RemoveAction.LABEL + }, + when: Constants.FileMatchOrMatchFocusKey, + group: 'search', + order: 2 +}); + +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: Constants.CopyMatchCommandId, + weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + when: Constants.FileMatchOrMatchFocusKey, + primary: KeyMod.CtrlCmd | KeyCode.KEY_C, + handler: copyMatchCommand +}); + +MenuRegistry.appendMenuItem(MenuId.SearchContext, { + command: { + id: Constants.CopyMatchCommandId, + title: nls.localize('copyMatchLabel', "Copy") + }, + when: Constants.FileMatchOrMatchFocusKey, + group: 'search_2', + order: 1 +}); + +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: Constants.CopyPathCommandId, + weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + when: Constants.FileMatchOrFolderMatchFocusKey, + primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_C, + win: { + primary: KeyMod.Shift | KeyMod.Alt | KeyCode.KEY_C + }, + handler: copyPathCommand +}); + +MenuRegistry.appendMenuItem(MenuId.SearchContext, { + command: { + id: Constants.CopyPathCommandId, + title: nls.localize('copyPathLabel', "Copy Path") + }, + when: Constants.FileMatchOrFolderMatchFocusKey, + group: 'search_2', + order: 2 +}); + +MenuRegistry.appendMenuItem(MenuId.SearchContext, { + command: { + id: Constants.CopyAllCommandId, + title: nls.localize('copyAllLabel', "Copy All") + }, + when: Constants.HasSearchResults, + group: 'search_2', + order: 3 +}); + +CommandsRegistry.registerCommand({ + id: Constants.CopyAllCommandId, + handler: copyAllCommand +}); + +CommandsRegistry.registerCommand({ + id: Constants.ToggleSearchViewPositionCommandId, + handler: (accessor) => { + const configurationService = accessor.get(IConfigurationService); + const currentValue = configurationService.getValue('search').location; + const toggleValue = currentValue === 'sidebar' ? 'panel' : 'sidebar'; + + configurationService.updateValue('search.location', toggleValue); + } +}); + +const toggleSearchViewPositionLabel = nls.localize('toggleSearchViewPositionLabel', "Toggle Search View Position"); +const ToggleSearchViewPositionCommand: ICommandAction = { + id: Constants.ToggleSearchViewPositionCommandId, + title: toggleSearchViewPositionLabel, + category +}; +MenuRegistry.addCommand(ToggleSearchViewPositionCommand); +MenuRegistry.appendMenuItem(MenuId.SearchContext, { + command: ToggleSearchViewPositionCommand, + when: Constants.SearchViewVisibleKey, + group: 'search_9', + order: 1 +}); + const FIND_IN_FOLDER_ID = 'filesExplorer.findInFolder'; CommandsRegistry.registerCommand({ id: FIND_IN_FOLDER_ID, @@ -297,7 +420,7 @@ Registry.as(ViewletExtensions.Viewlets).registerViewlet(new Vie VIEW_ID, nls.localize('name', "Search"), 'search', - 10 + 1 )); Registry.as(PanelExtensions.Panels).registerPanel(new PanelDescriptor( @@ -313,7 +436,6 @@ Registry.as(WorkbenchExtensions.Workbench).regi // Actions const registry = Registry.as(ActionExtensions.WorkbenchActions); -const category = nls.localize('search', "Search"); registry.registerWorkbenchAction(new SyncActionDescriptor(FindInFilesAction, VIEW_ID, nls.localize('showSearchViewl', "Show Search"), { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_F }, Constants.SearchViewVisibleKey.toNegated()), 'View: Show Search', nls.localize('view', "View")); @@ -361,6 +483,9 @@ registry.registerWorkbenchAction(new SyncActionDescriptor(ShowPreviousSearchTerm registry.registerWorkbenchAction(new SyncActionDescriptor(ShowNextSearchIncludeAction, ShowNextSearchIncludeAction.ID, ShowNextSearchIncludeAction.LABEL, ShowNextFindTermKeybinding, ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.PatternIncludesFocusedKey)), 'Search: Show Next Search Include Pattern', category); registry.registerWorkbenchAction(new SyncActionDescriptor(ShowPreviousSearchIncludeAction, ShowPreviousSearchIncludeAction.ID, ShowPreviousSearchIncludeAction.LABEL, ShowPreviousFindTermKeybinding, ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.PatternIncludesFocusedKey)), 'Search: Show Previous Search Include Pattern', category); +registry.registerWorkbenchAction(new SyncActionDescriptor(ShowNextSearchExcludeAction, ShowNextSearchExcludeAction.ID, ShowNextSearchExcludeAction.LABEL, ShowNextFindTermKeybinding, ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.PatternExcludesFocusedKey)), 'Search: Show Next Search Exclude Pattern', category); +registry.registerWorkbenchAction(new SyncActionDescriptor(ShowPreviousSearchExcludeAction, ShowPreviousSearchExcludeAction.ID, ShowPreviousSearchExcludeAction.LABEL, ShowPreviousFindTermKeybinding, ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.PatternExcludesFocusedKey)), 'Search: Show Previous Search Exclude Pattern', category); + registry.registerWorkbenchAction(new SyncActionDescriptor(CollapseDeepestExpandedLevelAction, CollapseDeepestExpandedLevelAction.ID, CollapseDeepestExpandedLevelAction.LABEL), 'Search: Collapse All', category); registry.registerWorkbenchAction(new SyncActionDescriptor(ShowAllSymbolsAction, ShowAllSymbolsAction.ID, ShowAllSymbolsAction.LABEL, { primary: KeyMod.CtrlCmd | KeyCode.KEY_T }), 'Go to Symbol in Workspace...'); @@ -461,7 +586,7 @@ configurationRegistry.registerConfiguration({ 'search.location': { enum: ['sidebar', 'panel'], default: 'sidebar', - description: nls.localize('search.location', "Preview: controls if the search will be shown as a view in the sidebar or as a panel in the panel area for more horizontal space. Next release search in panel will have improved horizontal layout and this will no longer be a preview."), + description: nls.localize('search.location', "Controls if the search will be shown as a view in the sidebar or as a panel in the panel area for more horizontal space. Next release search in panel will have improved horizontal layout and this will no longer be a preview."), }, } }); diff --git a/src/vs/workbench/parts/stats/node/workspaceStats.ts b/src/vs/workbench/parts/stats/node/workspaceStats.ts index 71c6572e0b9..686fea24971 100644 --- a/src/vs/workbench/parts/stats/node/workspaceStats.ts +++ b/src/vs/workbench/parts/stats/node/workspaceStats.ts @@ -359,7 +359,7 @@ export class WorkspaceStats implements IWorkbenchContribution { set.forEach(item => list.push(item)); /* __GDPR__ "workspace.remotes" : { - "domains" : { "classification": "CustomerContent", "purpose": "FeatureInsight" } + "domains" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } } */ this.telemetryService.publicLog('workspace.remotes', { domains: list.sort() }); diff --git a/src/vs/workbench/parts/surveys/electron-browser/languageSurveys.contribution.ts b/src/vs/workbench/parts/surveys/electron-browser/languageSurveys.contribution.ts index e4f90adef38..df9e5b138ce 100644 --- a/src/vs/workbench/parts/surveys/electron-browser/languageSurveys.contribution.ts +++ b/src/vs/workbench/parts/surveys/electron-browser/languageSurveys.contribution.ts @@ -12,12 +12,12 @@ import { IWorkbenchContributionsRegistry, IWorkbenchContribution, Extensions as import { Registry } from 'vs/platform/registry/common/platform'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { FileChangeType, IFileService } from 'vs/platform/files/common/files'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import pkg from 'vs/platform/node/package'; import product, { ISurveyData } from 'vs/platform/node/product'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; -import { Severity, INotificationService, PromptOption } from 'vs/platform/notification/common/notification'; +import { Severity, INotificationService } from 'vs/platform/notification/common/notification'; +import { ITextFileService, StateChange } from 'vs/workbench/services/textfile/common/textfiles'; class LanguageSurvey { @@ -27,8 +27,8 @@ class LanguageSurvey { storageService: IStorageService, notificationService: INotificationService, telemetryService: ITelemetryService, - fileService: IFileService, - modelService: IModelService + modelService: IModelService, + textFileService: ITextFileService ) { const SESSION_COUNT_KEY = `${data.surveyId}.sessionCount`; const LAST_SESSION_DATE_KEY = `${data.surveyId}.lastSessionDate`; @@ -44,9 +44,9 @@ class LanguageSurvey { const date = new Date().toDateString(); if (storageService.getInteger(EDITED_LANGUAGE_COUNT_KEY, StorageScope.GLOBAL, 0) < data.editCount) { - fileService.onFileChanges(e => { - e.getUpdated().forEach(event => { - if (event.type === FileChangeType.UPDATED) { + textFileService.models.onModelsSaved(e => { + e.forEach(event => { + if (event.kind === StateChange.SAVED) { const model = modelService.getModel(event.resource); if (model && model.getModeId() === data.languageId && date !== storageService.get(EDITED_LANGUAGE_DATE_KEY, StorageScope.GLOBAL)) { const editedCount = storageService.getInteger(EDITED_LANGUAGE_COUNT_KEY, StorageScope.GLOBAL, 0) + 1; @@ -87,30 +87,36 @@ class LanguageSurvey { // __GDPR__TODO__ Need to move away from dynamic event names as those cannot be registered statically telemetryService.publicLog(`${data.surveyId}.survey/userAsked`); - const choices: PromptOption[] = [nls.localize('takeShortSurvey', "Take Short Survey"), nls.localize('remindLater', "Remind Me later"), { label: nls.localize('neverAgain', "Don't Show Again") }]; - notificationService.prompt(Severity.Info, nls.localize('helpUs', "Help us improve our support for {0}", data.languageId), choices).then(choice => { - switch (choice) { - case 0 /* Take Survey */: + notificationService.prompt( + Severity.Info, + nls.localize('helpUs', "Help us improve our support for {0}", data.languageId), + [{ + label: nls.localize('takeShortSurvey', "Take Short Survey"), + run: () => { telemetryService.publicLog(`${data.surveyId}.survey/takeShortSurvey`); telemetryService.getTelemetryInfo().then(info => { window.open(`${data.surveyUrl}?o=${encodeURIComponent(process.platform)}&v=${encodeURIComponent(pkg.version)}&m=${encodeURIComponent(info.machineId)}`); storageService.store(IS_CANDIDATE_KEY, false, StorageScope.GLOBAL); storageService.store(SKIP_VERSION_KEY, pkg.version, StorageScope.GLOBAL); }); - break; - case 1 /* Remind Later */: + } + }, { + label: nls.localize('remindLater', "Remind Me later"), + run: () => { telemetryService.publicLog(`${data.surveyId}.survey/remindMeLater`); storageService.store(SESSION_COUNT_KEY, sessionCount - 3, StorageScope.GLOBAL); - break; - case 2 /* Never show again */: + } + }, { + label: nls.localize('neverAgain', "Don't Show Again"), + isSecondary: true, + run: () => { telemetryService.publicLog(`${data.surveyId}.survey/dontShowAgain`); storageService.store(IS_CANDIDATE_KEY, false, StorageScope.GLOBAL); storageService.store(SKIP_VERSION_KEY, pkg.version, StorageScope.GLOBAL); - break; - } - }); + } + }] + ); } - } class LanguageSurveysContribution implements IWorkbenchContribution { @@ -120,15 +126,15 @@ class LanguageSurveysContribution implements IWorkbenchContribution { @IStorageService storageService: IStorageService, @INotificationService notificationService: INotificationService, @ITelemetryService telemetryService: ITelemetryService, - @IFileService fileService: IFileService, - @IModelService modelService: IModelService + @IModelService modelService: IModelService, + @ITextFileService textFileService: ITextFileService ) { product.surveys.filter(surveyData => surveyData.surveyId && surveyData.editCount && surveyData.languageId && surveyData.surveyUrl && surveyData.userProbability).map(surveyData => - new LanguageSurvey(surveyData, instantiationService, storageService, notificationService, telemetryService, fileService, modelService)); + new LanguageSurvey(surveyData, instantiationService, storageService, notificationService, telemetryService, modelService, textFileService)); } } if (language === 'en' && product.surveys && product.surveys.length) { const workbenchRegistry = Registry.as(WorkbenchExtensions.Workbench); workbenchRegistry.registerWorkbenchContribution(LanguageSurveysContribution, LifecyclePhase.Running); -} \ No newline at end of file +} diff --git a/src/vs/workbench/parts/surveys/electron-browser/nps.contribution.ts b/src/vs/workbench/parts/surveys/electron-browser/nps.contribution.ts index 74d76dfd627..5313485e412 100644 --- a/src/vs/workbench/parts/surveys/electron-browser/nps.contribution.ts +++ b/src/vs/workbench/parts/surveys/electron-browser/nps.contribution.ts @@ -15,7 +15,7 @@ import { IStorageService, StorageScope } from 'vs/platform/storage/common/storag import pkg from 'vs/platform/node/package'; import product from 'vs/platform/node/product'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; -import { Severity, INotificationService, PromptOption } from 'vs/platform/notification/common/notification'; +import { Severity, INotificationService } from 'vs/platform/notification/common/notification'; const PROBABILITY = 0.15; const SESSION_COUNT_KEY = 'nps/sessionCount'; @@ -62,25 +62,30 @@ class NPSContribution implements IWorkbenchContribution { return; } - const choices: PromptOption[] = [nls.localize('takeSurvey', "Take Survey"), nls.localize('remindLater', "Remind Me later"), { label: nls.localize('neverAgain', "Don't Show Again") }]; - notificationService.prompt(Severity.Info, nls.localize('surveyQuestion', "Do you mind taking a quick feedback survey?"), choices).then(choice => { - switch (choice) { - case 0 /* Take Survey */: + notificationService.prompt( + Severity.Info, + nls.localize('surveyQuestion', "Do you mind taking a quick feedback survey?"), + [{ + label: nls.localize('takeSurvey', "Take Survey"), + run: () => { telemetryService.getTelemetryInfo().then(info => { window.open(`${product.npsSurveyUrl}?o=${encodeURIComponent(process.platform)}&v=${encodeURIComponent(pkg.version)}&m=${encodeURIComponent(info.machineId)}`); storageService.store(IS_CANDIDATE_KEY, false, StorageScope.GLOBAL); storageService.store(SKIP_VERSION_KEY, pkg.version, StorageScope.GLOBAL); }); - break; - case 1 /* Remind Later */: - storageService.store(SESSION_COUNT_KEY, sessionCount - 3, StorageScope.GLOBAL); - break; - case 2 /* Never show again */: + } + }, { + label: nls.localize('remindLater', "Remind Me later"), + run: () => storageService.store(SESSION_COUNT_KEY, sessionCount - 3, StorageScope.GLOBAL) + }, { + label: nls.localize('neverAgain', "Don't Show Again"), + isSecondary: true, + run: () => { storageService.store(IS_CANDIDATE_KEY, false, StorageScope.GLOBAL); storageService.store(SKIP_VERSION_KEY, pkg.version, StorageScope.GLOBAL); - break; - } - }); + } + }] + ); } } diff --git a/src/vs/workbench/parts/tasks/common/problemMatcher.ts b/src/vs/workbench/parts/tasks/common/problemMatcher.ts index 5a137c0ad71..9ff40a2baf6 100644 --- a/src/vs/workbench/parts/tasks/common/problemMatcher.ts +++ b/src/vs/workbench/parts/tasks/common/problemMatcher.ts @@ -302,6 +302,9 @@ abstract class AbstractLineMatcher implements ILineMatcher { } private getLocation(data: ProblemData): Location { + if (data.kind === ProblemLocationKind.File) { + return this.createLocation(0, 0, 0, 0); + } if (data.location) { return this.parseLocationInfo(data.location); } @@ -383,7 +386,7 @@ class SingleLineMatcher extends AbstractLineMatcher { public handle(lines: string[], start: number = 0): HandleResult { Assert.ok(lines.length - start === 1); let data: ProblemData = Object.create(null); - if (this.pattern.kind) { + if (this.pattern.kind !== void 0) { data.kind = this.pattern.kind; } let matches = this.pattern.regexp.exec(lines[start]); diff --git a/src/vs/workbench/parts/tasks/common/taskService.ts b/src/vs/workbench/parts/tasks/common/taskService.ts index 53aad958232..4a889b479eb 100644 --- a/src/vs/workbench/parts/tasks/common/taskService.ts +++ b/src/vs/workbench/parts/tasks/common/taskService.ts @@ -32,6 +32,11 @@ export interface CustomizationProperties { isBackground?: boolean; } +export interface TaskFilter { + version?: string; + type?: string; +} + export interface ITaskService { _serviceBrand: any; onDidStateChange: Event; @@ -45,7 +50,7 @@ export interface ITaskService { restart(task: Task): void; terminate(task: Task): TPromise; terminateAll(): TPromise; - tasks(): TPromise; + tasks(filter?: TaskFilter): TPromise; /** * @param alias The task's name, label or defined identifier. */ diff --git a/src/vs/workbench/parts/tasks/common/tasks.ts b/src/vs/workbench/parts/tasks/common/tasks.ts index b5a93a7cb63..8fd3e0fc004 100644 --- a/src/vs/workbench/parts/tasks/common/tasks.ts +++ b/src/vs/workbench/parts/tasks/common/tasks.ts @@ -34,6 +34,9 @@ export enum ShellQuoting { export namespace ShellQuoting { export function from(this: void, value: string): ShellQuoting { + if (!value) { + return ShellQuoting.Strong; + } switch (value.toLowerCase()) { case 'escape': return ShellQuoting.Escape; @@ -611,25 +614,6 @@ export namespace Task { } } - export function getTaskItem(task: Task): TaskItem { - let folder: IWorkspaceFolder = Task.getWorkspaceFolder(task); - let definition: TaskIdentifier; - if (ContributedTask.is(task)) { - definition = task.defines; - } else if (CustomTask.is(task) && task.command !== void 0) { - definition = CustomTask.getDefinition(task); - } else { - return undefined; - } - let result: TaskItem = { - id: task._id, - label: task._label, - definition: definition, - workspaceFolder: folder - }; - return result; - } - export function getTaskDefinition(task: Task): TaskIdentifier { if (ContributedTask.is(task)) { return task.defines; @@ -642,21 +626,16 @@ export namespace Task { export function getTaskExecution(task: Task): TaskExecution { let result: TaskExecution = { - id: task._id + id: task._id, + task: task }; return result; } } -export interface TaskItem { - id: string; - label: string; - definition: TaskIdentifier; - workspaceFolder: IWorkspaceFolder; -} - export interface TaskExecution { id: string; + task: Task; } export enum ExecutionEngine { diff --git a/src/vs/workbench/parts/tasks/electron-browser/jsonSchema_v2.ts b/src/vs/workbench/parts/tasks/electron-browser/jsonSchema_v2.ts index 8c340e0a9f8..191204144ef 100644 --- a/src/vs/workbench/parts/tasks/electron-browser/jsonSchema_v2.ts +++ b/src/vs/workbench/parts/tasks/electron-browser/jsonSchema_v2.ts @@ -156,6 +156,7 @@ const command: IJSONSchema = { }, { type: 'object', + required: ['value', 'quoting'], properties: { value: { type: 'string', @@ -188,6 +189,7 @@ const args: IJSONSchema = { }, { type: 'object', + required: ['value', 'quoting'], properties: { value: { type: 'string', 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 6ce2cb1fe71..64b8acbae0d 100644 --- a/src/vs/workbench/parts/tasks/electron-browser/task.contribution.ts +++ b/src/vs/workbench/parts/tasks/electron-browser/task.contribution.ts @@ -7,6 +7,7 @@ import 'vs/css!./media/task.contribution'; import * as nls from 'vs/nls'; +import * as semver from 'semver'; import { QuickOpenHandler } from 'vs/workbench/parts/tasks/browser/taskQuickOpen'; import { TPromise } from 'vs/base/common/winjs.base'; @@ -17,8 +18,7 @@ import { IStringDictionary } from 'vs/base/common/collections'; import { Action } from 'vs/base/common/actions'; import * as Dom from 'vs/base/browser/dom'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { Event, Emitter, once } from 'vs/base/common/event'; -import * as Builder from 'vs/base/browser/builder'; +import { Event, Emitter } from 'vs/base/common/event'; import * as Types from 'vs/base/common/types'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { TerminateResponseCode } from 'vs/base/common/processes'; @@ -74,9 +74,9 @@ import { ITaskSystem, ITaskResolver, ITaskSummary, TaskExecuteKind, TaskError, T import { Task, CustomTask, ConfiguringTask, ContributedTask, InMemoryTask, TaskEvent, TaskEventKind, TaskSet, TaskGroup, GroupType, ExecutionEngine, JsonSchemaVersion, TaskSourceKind, - TaskIdentifier, TaskSorter, TaskItem + TaskIdentifier, TaskSorter } from 'vs/workbench/parts/tasks/common/tasks'; -import { ITaskService, ITaskProvider, RunOptions, CustomizationProperties } from 'vs/workbench/parts/tasks/common/taskService'; +import { ITaskService, ITaskProvider, RunOptions, CustomizationProperties, TaskFilter } from 'vs/workbench/parts/tasks/common/taskService'; import { getTemplates as getTaskTemplates } from 'vs/workbench/parts/tasks/common/taskTemplates'; import * as TaskConfig from '../node/taskConfiguration'; @@ -88,7 +88,6 @@ import { QuickOpenActionContributor } from '../browser/quickOpen'; import { Themable, STATUS_BAR_FOREGROUND, STATUS_BAR_NO_FOLDER_FOREGROUND } from 'vs/workbench/common/theme'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -let $ = Builder.$; let tasksCategory = nls.localize('tasksCategory', "Tasks"); namespace ConfigureTaskAction { @@ -146,6 +145,7 @@ class BuildStatusBarItem extends Themable implements IStatusbarItem { const infoTitle = n => nls.localize('totalInfos', "{0} Infos", n); Dom.addClass(element, 'task-statusbar-item'); + element.title = nls.localize('problems', "Problems"); Dom.addClass(label, 'task-statusbar-item-label'); element.appendChild(label); @@ -174,17 +174,16 @@ class BuildStatusBarItem extends Themable implements IStatusbarItem { Dom.addClass(infoIcon, 'mask-icon'); label.appendChild(infoIcon); this.icons.push(infoIcon); - $(infoIcon).hide(); + Dom.hide(infoIcon); Dom.addClass(info, 'task-statusbar-item-label-counter'); label.appendChild(info); - $(info).hide(); + Dom.hide(info); Dom.addClass(building, 'task-statusbar-item-building'); element.appendChild(building); building.innerHTML = nls.localize('building', 'Building...'); - $(building).hide(); - + Dom.hide(building); callOnDispose.push(Dom.addDisposableListener(label, 'click', (e: MouseEvent) => { const panel = this.panelService.getActivePanel(); @@ -205,11 +204,11 @@ class BuildStatusBarItem extends Themable implements IStatusbarItem { if (stats.infos > 0) { info.innerHTML = packNumber(stats.infos); info.title = infoIcon.title = infoTitle(stats.infos); - $(info).show(); - $(infoIcon).show(); + Dom.show(info); + Dom.show(infoIcon); } else { - $(info).hide(); - $(infoIcon).hide(); + Dom.hide(info); + Dom.hide(infoIcon); } }; @@ -225,7 +224,7 @@ class BuildStatusBarItem extends Themable implements IStatusbarItem { case TaskEventKind.Active: this.activeCount++; if (this.activeCount === 1) { - $(building).show(); + Dom.show(building); } break; case TaskEventKind.Inactive: @@ -234,13 +233,13 @@ class BuildStatusBarItem extends Themable implements IStatusbarItem { if (this.activeCount > 0) { this.activeCount--; if (this.activeCount === 0) { - $(building).hide(); + Dom.hide(building); } } break; case TaskEventKind.Terminated: if (this.activeCount !== 0) { - $(building).hide(); + Dom.hide(building); this.activeCount = 0; } break; @@ -298,7 +297,7 @@ class TaskStatusBarItem extends Themable implements IStatusbarItem { let label = new OcticonLabel(labelElement); label.title = nls.localize('runningTasks', "Show Running Tasks"); - $(element).hide(); + Dom.hide(element); callOnDispose.push(Dom.addDisposableListener(labelElement, 'click', (e: MouseEvent) => { (this.taskService as TaskService).runShowTasks(); @@ -307,10 +306,10 @@ class TaskStatusBarItem extends Themable implements IStatusbarItem { let updateStatus = (): void => { this.taskService.getActiveTasks().then(tasks => { if (tasks.length === 0) { - $(element).hide(); + Dom.hide(element); } else { label.text = `$(tools) ${tasks.length}`; - $(element).show(); + Dom.show(element); } }); }; @@ -497,14 +496,17 @@ class TaskService implements ITaskService { let folderSetup = this.computeWorkspaceFolderSetup(); if (this.executionEngine !== folderSetup[2]) { if (this._taskSystem && this._taskSystem.getActiveTasks().length > 0) { - this.notificationService.prompt(Severity.Info, nls.localize( - 'TaskSystem.noHotSwap', - 'Changing the task execution engine with an active task running requires to reload the Window' - ), [nls.localize('reloadWindow', "Reload Window")]).then(choice => { - if (choice === 0) { - this._windowService.reloadWindow(); - } - }); + this.notificationService.prompt( + Severity.Info, + nls.localize( + 'TaskSystem.noHotSwap', + 'Changing the task execution engine with an active task running requires to reload the Window' + ), + [{ + label: nls.localize('reloadWindow', "Reload Window"), + run: () => this._windowService.reloadWindow() + }] + ); return; } else { this.disposeTaskSystemListeners(); @@ -579,19 +581,6 @@ class TaskService implements ITaskService { CommandsRegistry.registerCommand('workbench.action.tasks.showTasks', () => { this.runShowTasks(); }); - - CommandsRegistry.registerCommand('_executeTaskProvider', (accessor, args) => { - return this.tasks().then((tasks) => { - let result: TaskItem[] = []; - for (let task of tasks) { - let item = Task.getTaskItem(task); - if (item) { - result.push(item); - } - } - return result; - }); - }); } private get workspaceFolders(): IWorkspaceFolder[] { @@ -693,8 +682,28 @@ class TaskService implements ITaskService { }); } - public tasks(): TPromise { - return this.getGroupedTasks().then(result => result.all()); + public tasks(filter?: TaskFilter): TPromise { + let range = filter && filter.version ? filter.version : undefined; + let engine = this.executionEngine; + + if (range && ((semver.satisfies('0.1.0', range) && engine === ExecutionEngine.Terminal) || (semver.satisfies('2.0.0', range) && engine === ExecutionEngine.Process))) { + return TPromise.as([]); + } + return this.getGroupedTasks().then((map) => { + if (!filter || !filter.type) { + return map.all(); + } + let result: Task[] = []; + map.forEach((tasks) => { + for (let task of tasks) { + let definition = Task.getTaskDefinition(task); + if (definition && definition.type === filter.type) { + result.push(task); + } + } + }); + return result; + }); } public createSorter(): TaskSorter { @@ -1166,11 +1175,22 @@ class TaskService implements ITaskService { if (executeResult.kind === TaskExecuteKind.Active) { let active = executeResult.active; if (active.same) { + let message; if (active.background) { - this.notificationService.info(nls.localize('TaskSystem.activeSame.background', 'The task \'{0}\' is already active and in background mode. To terminate it use \'Terminate Task...\' from the Tasks menu.', Task.getQualifiedLabel(task))); + message = nls.localize('TaskSystem.activeSame.background', 'The task \'{0}\' is already active and in background mode.', Task.getQualifiedLabel(task)); } else { - this.notificationService.info(nls.localize('TaskSystem.activeSame.noBackground', 'The task \'{0}\' is already active. To terminate it use \'Terminate Task...\' from the Tasks menu.', Task.getQualifiedLabel(task))); + message = nls.localize('TaskSystem.activeSame.noBackground', 'The task \'{0}\' is already active.', Task.getQualifiedLabel(task)); } + this.notificationService.prompt(Severity.Info, message, + [{ + label: nls.localize('terminateTask', "Terminate Task"), + run: () => this.terminate(task) + }, + { + label: nls.localize('restartTask', "Restart Task"), + run: () => this.restart(task) + }] + ); } else { throw new TaskError(Severity.Warning, nls.localize('TaskSystem.active', 'There is already a task running. Terminate it first before executing another task.'), TaskErrors.RunningTask); } @@ -1617,15 +1637,6 @@ class TaskService implements ITaskService { }; } - private configureBuildTask(): Action { - let run = () => { this.runConfigureTasks(); return TPromise.as(undefined); }; - return new class extends Action { - constructor() { - super(ConfigureTaskAction.ID, ConfigureTaskAction.TEXT, undefined, true, run); - } - }; - } - public beforeShutdown(): boolean | TPromise { if (!this._taskSystem) { return false; @@ -1685,15 +1696,6 @@ class TaskService implements ITaskService { }); } - private getConfigureAction(code: TaskErrors): Action { - switch (code) { - case TaskErrors.NoBuildTask: - return this.configureBuildTask(); - default: - return this.configureAction(); - } - } - private handleError(err: any): void { let showOutput = true; if (err instanceof TaskError) { @@ -1701,14 +1703,16 @@ class TaskService implements ITaskService { let needsConfig = buildError.code === TaskErrors.NotConfigured || buildError.code === TaskErrors.NoBuildTask || buildError.code === TaskErrors.NoTestTask; let needsTerminate = buildError.code === TaskErrors.RunningTask; if (needsConfig || needsTerminate) { - let action: Action = needsConfig - ? this.getConfigureAction(buildError.code) - : new Action( - 'workbench.action.tasks.terminate', - nls.localize('TerminateAction.label', "Terminate Task"), - undefined, true, () => { this.runTerminateCommand(); return TPromise.wrap(undefined); }); - let handle = this.notificationService.notify({ severity: buildError.severity, message: buildError.message, actions: { primary: [action] } }); - once(handle.onDidDispose)(() => action.dispose()); + this.notificationService.prompt(buildError.severity, buildError.message, [{ + label: needsConfig ? ConfigureTaskAction.TEXT : nls.localize('TerminateAction.label', "Terminate Task"), + run: () => { + if (needsConfig) { + this.runConfigureTasks(); + } else { + this.runTerminateCommand(); + } + } + }]); } else { this.notificationService.notify({ severity: buildError.severity, message: buildError.message }); } @@ -1849,24 +1853,18 @@ class TaskService implements ITaskService { return TPromise.as(undefined); } - const action = new Action('dontShowAgain', nls.localize('TaskService.notAgain', 'Don\'t Show Again'), null, true, (notification: IDisposable) => { - this.storageService.store(TaskService.IgnoreTask010DonotShowAgain_key, true, StorageScope.WORKSPACE); - this.__showIgnoreMessage = false; - - // Hide notification - notification.dispose(); - - return TPromise.as(true); - }); - - const handle = this.notificationService.notify({ - severity: Severity.Info, - message: nls.localize('TaskService.ignoredFolder', 'The following workspace folders are ignored since they use task version 0.1.0: {0}', this.ignoredWorkspaceFolders.map(f => f.name).join(', ')), - actions: { - secondary: [action] - } - }); - once(handle.onDidDispose)(() => action.dispose()); + this.notificationService.prompt( + Severity.Info, + nls.localize('TaskService.ignoredFolder', 'The following workspace folders are ignored since they use task version 0.1.0: {0}', this.ignoredWorkspaceFolders.map(f => f.name).join(', ')), + [{ + label: nls.localize('TaskService.notAgain', 'Don\'t Show Again'), + isSecondary: true, + run: () => { + this.storageService.store(TaskService.IgnoreTask010DonotShowAgain_key, true, StorageScope.WORKSPACE); + this.__showIgnoreMessage = false; + } + }] + ); return TPromise.as(undefined); } diff --git a/src/vs/workbench/parts/tasks/electron-browser/terminalTaskSystem.ts b/src/vs/workbench/parts/tasks/electron-browser/terminalTaskSystem.ts index 17de814f26e..584292e76c0 100644 --- a/src/vs/workbench/parts/tasks/electron-browser/terminalTaskSystem.ts +++ b/src/vs/workbench/parts/tasks/electron-browser/terminalTaskSystem.ts @@ -62,18 +62,24 @@ export class TerminalTaskSystem implements ITaskSystem { 'powershell': { escape: { escapeChar: '`', - charsToEscape: ` ()` + charsToEscape: ' "\'()' }, strong: '\'', weak: '"' }, 'bash': { - escape: '\\', + escape: { + escapeChar: '\\', + charsToEscape: ' "\'' + }, strong: '\'', weak: '"' }, 'zsh': { - escape: '\\', + escape: { + escapeChar: '\\', + charsToEscape: ' "\'' + }, strong: '\'', weak: '"' } @@ -374,8 +380,8 @@ export class TerminalTaskSystem implements ITaskSystem { if (!terminal) { return TPromise.wrapError(new Error(`Failed to create terminal for task ${task._label}`)); } - this.terminalService.setActiveInstance(terminal); if (task.command.presentation.reveal === RevealKind.Always || (task.command.presentation.reveal === RevealKind.Silent && task.problemMatchers.length === 0)) { + this.terminalService.setActiveInstance(terminal); this.terminalService.showPanel(task.command.presentation.focus); } this.activeTasks[Task.getMapKey(task)] = { terminal, task, promise }; @@ -503,7 +509,7 @@ export class TerminalTaskSystem implements ITaskSystem { let cwd = options && options.cwd ? options.cwd : process.cwd(); // On Windows executed process must be described absolute. Since we allowed command without an // absolute path (e.g. "command": "node") we need to find the executable in the CWD or PATH. - let executable = Platform.isWindows && !isShellCommand ? this.findExecutable(commandExecutable, cwd) : commandExecutable; + let executable = Platform.isWindows && !isShellCommand ? this.findExecutable(commandExecutable, cwd, options) : commandExecutable; // When we have a process task there is no need to quote arguments. So we go ahead and take the string value. shellLaunchConfig = { @@ -565,7 +571,7 @@ export class TerminalTaskSystem implements ITaskSystem { return [terminalToReuse.terminal, commandExecutable]; } - const result = this.terminalService.createInstance(shellLaunchConfig); + const result = this.terminalService.createTerminal(shellLaunchConfig); const terminalKey = result.id.toString(); result.onDisposed((terminal) => { let terminalData = this.terminals[terminalKey]; @@ -682,23 +688,39 @@ export class TerminalTaskSystem implements ITaskSystem { return { command, args }; } - private findExecutable(command: string, cwd: string): string { + private findExecutable(command: string, cwd: string, options: CommandOptions): string { // If we have an absolute path then we take it. if (path.isAbsolute(command)) { return command; } let dir = path.dirname(command); if (dir !== '.') { - // We have a directory. Make the path absolute - // to the current working directory + // We have a directory and the directory is relative (see above). Make the path absolute + // to the current working directory. + return path.join(cwd, command); + } + let paths: string[] = undefined; + // The options can override the PATH. So consider that PATH if present. + if (options && options.env) { + // Path can be named in many different ways and for the execution it doesn't matter + for (let key of Object.keys(options.env)) { + if (key.toLowerCase() === 'path') { + if (Types.isString(options.env[key])) { + paths = options.env[key].split(path.delimiter); + } + break; + } + } + } + if (paths === void 0 && Types.isString(process.env.PATH)) { + paths = process.env.PATH.split(path.delimiter); + } + // No PATH environment. Make path absolute to the cwd. + if (paths === void 0 || paths.length === 0) { return path.join(cwd, command); } // We have a simple file name. We get the path variable from the env // and try to find the executable on the path. - if (!process.env.PATH) { - return command; - } - let paths: string[] = (process.env.PATH as string).split(path.delimiter); for (let pathEntry of paths) { // The path entry is absolute. let fullPath: string; diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminalFindWidget.ts b/src/vs/workbench/parts/terminal/browser/terminalFindWidget.ts similarity index 100% rename from src/vs/workbench/parts/terminal/electron-browser/terminalFindWidget.ts rename to src/vs/workbench/parts/terminal/browser/terminalFindWidget.ts diff --git a/src/vs/workbench/parts/terminal/browser/terminalQuickOpen.ts b/src/vs/workbench/parts/terminal/browser/terminalQuickOpen.ts index 984073baa7c..38ef49d495a 100644 --- a/src/vs/workbench/parts/terminal/browser/terminalQuickOpen.ts +++ b/src/vs/workbench/parts/terminal/browser/terminalQuickOpen.ts @@ -2,7 +2,6 @@ * 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 nls from 'vs/nls'; import { TPromise } from 'vs/base/common/winjs.base'; diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminalTab.ts b/src/vs/workbench/parts/terminal/browser/terminalTab.ts similarity index 92% rename from src/vs/workbench/parts/terminal/electron-browser/terminalTab.ts rename to src/vs/workbench/parts/terminal/browser/terminalTab.ts index aaf35637304..81d906b9fc2 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/terminalTab.ts +++ b/src/vs/workbench/parts/terminal/browser/terminalTab.ts @@ -3,11 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ITerminalInstance, IShellLaunchConfig, ITerminalTab, Direction, ITerminalService } from 'vs/workbench/parts/terminal/common/terminal'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { TerminalConfigHelper } from 'vs/workbench/parts/terminal/electron-browser/terminalConfigHelper'; +import { ITerminalInstance, IShellLaunchConfig, ITerminalTab, Direction, ITerminalService, ITerminalConfigHelper } from 'vs/workbench/parts/terminal/common/terminal'; import { IContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { TerminalInstance } from 'vs/workbench/parts/terminal/electron-browser/terminalInstance'; import { Event, Emitter, anyEvent } from 'vs/base/common/event'; import { IDisposable, Disposable } from 'vs/base/common/lifecycle'; import { SplitView, Orientation, IView } from 'vs/base/browser/ui/splitview/splitview'; @@ -149,22 +146,16 @@ class SplitPaneContainer { } public layout(width: number, height: number): void { - const resizeCallback = () => { - this._width = width; - this._height = height; - if (this.orientation === Orientation.HORIZONTAL) { - this._children.forEach(c => c.orthogonalLayout(height)); - this._splitView.layout(width); - } else { - this._children.forEach(c => c.orthogonalLayout(width)); - this._splitView.layout(height); - } - }; - - if (this._isManuallySized) { - resizeCallback(); + this._width = width; + this._height = height; + if (this.orientation === Orientation.HORIZONTAL) { + this._children.forEach(c => c.orthogonalLayout(height)); + this._splitView.layout(width); } else { - this._withDisabledLayout(resizeCallback); + this._children.forEach(c => c.orthogonalLayout(width)); + this._splitView.layout(height); + } + if (!this._isManuallySized) { this.resetSize(); } } @@ -261,10 +252,9 @@ export class TerminalTab extends Disposable implements ITerminalTab { constructor( terminalFocusContextKey: IContextKey, - configHelper: TerminalConfigHelper, + configHelper: ITerminalConfigHelper, private _container: HTMLElement, shellLaunchConfig: IShellLaunchConfig, - @IInstantiationService private readonly _instantiationService: IInstantiationService, @ITerminalService private readonly _terminalService: ITerminalService, @IPartService private readonly _partService: IPartService ) { @@ -272,11 +262,12 @@ export class TerminalTab extends Disposable implements ITerminalTab { this._onDisposed = new Emitter(); this._onInstancesChanged = new Emitter(); - const instance = this._instantiationService.createInstance(TerminalInstance, + const instance = this._terminalService.createInstance( terminalFocusContextKey, configHelper, undefined, - shellLaunchConfig); + shellLaunchConfig, + true); this._terminalInstances.push(instance); this._initInstanceListeners(instance); this._activeInstanceIndex = 0; @@ -398,14 +389,15 @@ export class TerminalTab extends Disposable implements ITerminalTab { public split( terminalFocusContextKey: IContextKey, - configHelper: TerminalConfigHelper, + configHelper: ITerminalConfigHelper, shellLaunchConfig: IShellLaunchConfig ): ITerminalInstance { - const instance = this._instantiationService.createInstance(TerminalInstance, + const instance = this._terminalService.createInstance( terminalFocusContextKey, configHelper, undefined, - shellLaunchConfig); + shellLaunchConfig, + true); this._terminalInstances.splice(this._activeInstanceIndex + 1, 0, instance); this._initInstanceListeners(instance); this._setActiveInstance(instance); diff --git a/src/vs/workbench/parts/terminal/common/terminal.ts b/src/vs/workbench/parts/terminal/common/terminal.ts index 62dc8f0a702..dfe9d89cc12 100644 --- a/src/vs/workbench/parts/terminal/common/terminal.ts +++ b/src/vs/workbench/parts/terminal/common/terminal.ts @@ -2,7 +2,6 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; import { Event } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; @@ -104,26 +103,35 @@ export interface ITerminalFont { } export interface IShellLaunchConfig { - /** The name of the terminal, if this is not set the name of the process will be used. */ + /** + * The name of the terminal, if this is not set the name of the process will be used. + */ name?: string; - /** The shell executable (bash, cmd, etc.). */ + + /** + * The shell executable (bash, cmd, etc.). + */ executable?: string; + /** * The CLI arguments to use with executable, a string[] is in argv format and will be escaped, * a string is in "CommandLine" pre-escaped format and will be used as is. The string option is * only supported on Windows and will throw an exception if used on macOS or Linux. */ args?: string[] | string; + /** * The current working directory of the terminal, this overrides the `terminal.integrated.cwd` * settings key. */ cwd?: string; + /** * A custom environment for the terminal, if this is not set the environment will be inherited * from the VS Code process. */ env?: { [key: string]: string }; + /** * Whether to ignore a custom cwd from the `terminal.integrated.cwd` settings key (eg. if the * shell is being launched by an extension). @@ -149,14 +157,26 @@ export interface ITerminalService { configHelper: ITerminalConfigHelper; onActiveTabChanged: Event; onTabDisposed: Event; + onInstanceCreated: Event; onInstanceDisposed: Event; onInstanceProcessIdReady: Event; + onInstanceRequestExtHostProcess: Event; onInstancesChanged: Event; onInstanceTitleChanged: Event; terminalInstances: ITerminalInstance[]; terminalTabs: ITerminalTab[]; - createInstance(shell?: IShellLaunchConfig, wasNewTerminalAction?: boolean): ITerminalInstance; + /** + * Creates a terminal. + * @param shell The shell launch configuration to use. + * @param wasNewTerminalAction Whether this was triggered by a new terminal action, if so a + * default shell selection dialog may display. + */ + createTerminal(shell?: IShellLaunchConfig, wasNewTerminalAction?: boolean): ITerminalInstance; + /** + * Creates a raw terminal instance, this should not be used outside of the terminal part. + */ + createInstance(terminalFocusContextKey: IContextKey, configHelper: ITerminalConfigHelper, container: HTMLElement, shellLaunchConfig: IShellLaunchConfig, doCreateProcess: boolean): ITerminalInstance; getInstanceFromId(terminalId: number): ITerminalInstance; getInstanceFromIndex(terminalIndex: number): ITerminalInstance; getTabLabels(): string[]; @@ -164,7 +184,7 @@ export interface ITerminalService { setActiveInstance(terminalInstance: ITerminalInstance): void; setActiveInstanceByIndex(terminalIndex: number): void; getActiveOrCreateInstance(wasNewTerminalAction?: boolean): ITerminalInstance; - splitInstance(instance: ITerminalInstance): void; + splitInstance(instance: ITerminalInstance, shell?: IShellLaunchConfig): void; getActiveTab(): ITerminalTab; setActiveTabToNext(): void; @@ -181,6 +201,8 @@ export interface ITerminalService { setContainers(panelContainer: HTMLElement, terminalContainer: HTMLElement): void; selectDefaultWindowsShell(): TPromise; setWorkspaceShellAllowed(isAllowed: boolean): void; + + requestExtHostProcess(proxy: ITerminalProcessExtHostProxy, shellLaunchConfig: IShellLaunchConfig, cols: number, rows: number): void; } export const enum Direction { @@ -216,9 +238,10 @@ export interface ITerminalInstance { id: number; /** - * The process ID of the shell process. + * The process ID of the shell process, this is undefined when there is no process associated + * with this terminal. */ - processId: number; + processId: number | undefined; /** * An event that fires when the terminal instance's title changes. @@ -234,6 +257,10 @@ export interface ITerminalInstance { onProcessIdReady: Event; + onRequestExtHostProcess: Event; + + processReady: TPromise; + /** * The title of the terminal. This is either title or the process currently running or an * explicit name given to the terminal instance through the extension API. @@ -448,8 +475,65 @@ export interface ITerminalInstance { } export interface ITerminalCommandTracker { - focusPreviousCommand(): void; - focusNextCommand(): void; + scrollToPreviousCommand(): void; + scrollToNextCommand(): void; selectToPreviousCommand(): void; selectToNextCommand(): void; +} + +export interface ITerminalProcessManager extends IDisposable { + readonly processState: ProcessState; + readonly ptyProcessReady: TPromise; + readonly shellProcessId: number; + readonly initialCwd: string; + + readonly onProcessReady: Event; + readonly onProcessData: Event; + readonly onProcessTitle: Event; + readonly onProcessExit: Event; + + addDisposable(disposable: IDisposable); + createProcess(shellLaunchConfig: IShellLaunchConfig, cols: number, rows: number); + write(data: string): void; + setDimensions(cols: number, rows: number): void; +} + +export enum ProcessState { + // The process has not been initialized yet. + UNINITIALIZED, + // The process is currently launching, the process is marked as launching + // for a short duration after being created and is helpful to indicate + // whether the process died as a result of bad shell and args. + LAUNCHING, + // The process is running normally. + RUNNING, + // The process was killed during launch, likely as a result of bad shell and + // args. + KILLED_DURING_LAUNCH, + // The process was killed by the user (the event originated from VS Code). + KILLED_BY_USER, + // The process was killed by itself, for example the shell crashed or `exit` + // was run. + KILLED_BY_PROCESS +} + + +export interface ITerminalProcessExtHostProxy extends IDisposable { + readonly terminalId: number; + + emitData(data: string): void; + emitTitle(title: string): void; + emitPid(pid: number): void; + emitExit(exitCode: number): void; + + onInput(listener: (data: string) => void): void; + onResize(listener: (cols: number, rows: number) => void): void; + onShutdown(listener: () => void): void; +} + +export interface ITerminalProcessExtHostRequest { + proxy: ITerminalProcessExtHostProxy; + shellLaunchConfig: IShellLaunchConfig; + cols: number; + rows: number; } \ No newline at end of file diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminalColorRegistry.ts b/src/vs/workbench/parts/terminal/common/terminalColorRegistry.ts similarity index 99% rename from src/vs/workbench/parts/terminal/electron-browser/terminalColorRegistry.ts rename to src/vs/workbench/parts/terminal/common/terminalColorRegistry.ts index fc8a72b6980..0d5fb389afa 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/terminalColorRegistry.ts +++ b/src/vs/workbench/parts/terminal/common/terminalColorRegistry.ts @@ -165,5 +165,4 @@ export function registerColors(): void { let colorName = id.substring(13); ansiColorIdentifiers[entry.index] = registerColor(id, entry.defaults, nls.localize('terminal.ansiColor', '\'{0}\' ANSI color in the terminal.', colorName)); } - } diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminalCommands.ts b/src/vs/workbench/parts/terminal/common/terminalCommands.ts similarity index 100% rename from src/vs/workbench/parts/terminal/electron-browser/terminalCommands.ts rename to src/vs/workbench/parts/terminal/common/terminalCommands.ts diff --git a/src/vs/workbench/parts/terminal/common/terminalService.ts b/src/vs/workbench/parts/terminal/common/terminalService.ts index 104182733c6..e294388516b 100644 --- a/src/vs/workbench/parts/terminal/common/terminalService.ts +++ b/src/vs/workbench/parts/terminal/common/terminalService.ts @@ -9,7 +9,7 @@ import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/c import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; import { IPartService } from 'vs/workbench/services/part/common/partService'; -import { ITerminalService, ITerminalInstance, IShellLaunchConfig, ITerminalConfigHelper, KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_VISIBLE, TERMINAL_PANEL_ID, ITerminalTab } from 'vs/workbench/parts/terminal/common/terminal'; +import { ITerminalService, ITerminalInstance, IShellLaunchConfig, ITerminalConfigHelper, KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_VISIBLE, TERMINAL_PANEL_ID, ITerminalTab, ITerminalProcessExtHostProxy, ITerminalProcessExtHostRequest } from 'vs/workbench/parts/terminal/common/terminal'; import { TPromise } from 'vs/base/common/winjs.base'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; @@ -22,27 +22,32 @@ export abstract class TerminalService implements ITerminalService { protected _terminalFocusContextKey: IContextKey; protected _findWidgetVisible: IContextKey; protected _terminalContainer: HTMLElement; - protected _onInstancesChanged: Emitter; - protected _onTabDisposed: Emitter; - protected _onInstanceDisposed: Emitter; - protected _onInstanceProcessIdReady: Emitter; - protected _onInstanceTitleChanged: Emitter; protected _terminalTabs: ITerminalTab[]; protected abstract _terminalInstances: ITerminalInstance[]; private _activeTabIndex: number; - private readonly _onActiveTabChanged: Emitter; public get activeTabIndex(): number { return this._activeTabIndex; } - public get onActiveTabChanged(): Event { return this._onActiveTabChanged.event; } - public get onTabDisposed(): Event { return this._onTabDisposed.event; } - public get onInstanceDisposed(): Event { return this._onInstanceDisposed.event; } - public get onInstanceProcessIdReady(): Event { return this._onInstanceProcessIdReady.event; } - public get onInstanceTitleChanged(): Event { return this._onInstanceTitleChanged.event; } - public get onInstancesChanged(): Event { return this._onInstancesChanged.event; } public get terminalInstances(): ITerminalInstance[] { return this._terminalInstances; } public get terminalTabs(): ITerminalTab[] { return this._terminalTabs; } + private readonly _onActiveTabChanged: Emitter = new Emitter(); + public get onActiveTabChanged(): Event { return this._onActiveTabChanged.event; } + protected readonly _onInstanceCreated: Emitter = new Emitter(); + public get onInstanceCreated(): Event { return this._onInstanceCreated.event; } + protected readonly _onInstanceDisposed: Emitter = new Emitter(); + public get onInstanceDisposed(): Event { return this._onInstanceDisposed.event; } + protected readonly _onInstanceProcessIdReady: Emitter = new Emitter(); + public get onInstanceProcessIdReady(): Event { return this._onInstanceProcessIdReady.event; } + protected readonly _onInstanceRequestExtHostProcess: Emitter = new Emitter(); + public get onInstanceRequestExtHostProcess(): Event { return this._onInstanceRequestExtHostProcess.event; } + protected readonly _onInstancesChanged: Emitter = new Emitter(); + public get onInstancesChanged(): Event { return this._onInstancesChanged.event; } + protected readonly _onInstanceTitleChanged: Emitter = new Emitter(); + public get onInstanceTitleChanged(): Event { return this._onInstanceTitleChanged.event; } + protected readonly _onTabDisposed: Emitter = new Emitter(); + public get onTabDisposed(): Event { return this._onTabDisposed.event; } + public abstract get configHelper(): ITerminalConfigHelper; constructor( @@ -55,13 +60,6 @@ export abstract class TerminalService implements ITerminalService { this._activeTabIndex = 0; this._isShuttingDown = false; - this._onActiveTabChanged = new Emitter(); - this._onTabDisposed = new Emitter(); - this._onInstanceDisposed = new Emitter(); - this._onInstanceProcessIdReady = new Emitter(); - this._onInstanceTitleChanged = new Emitter(); - this._onInstancesChanged = new Emitter(); - lifecycleService.onWillShutdown(event => event.veto(this._onWillShutdown())); lifecycleService.onShutdown(() => this._onShutdown()); this._terminalFocusContextKey = KEYBINDING_CONTEXT_TERMINAL_FOCUS.bindTo(this._contextKeyService); @@ -72,10 +70,12 @@ export abstract class TerminalService implements ITerminalService { } protected abstract _showTerminalCloseConfirmation(): TPromise; - public abstract createInstance(shell?: IShellLaunchConfig, wasNewTerminalAction?: boolean): ITerminalInstance; + public abstract createTerminal(shell?: IShellLaunchConfig, wasNewTerminalAction?: boolean): ITerminalInstance; + public abstract createInstance(terminalFocusContextKey: IContextKey, configHelper: ITerminalConfigHelper, container: HTMLElement, shellLaunchConfig: IShellLaunchConfig, doCreateProcess: boolean): ITerminalInstance; public abstract getActiveOrCreateInstance(wasNewTerminalAction?: boolean): ITerminalInstance; public abstract selectDefaultWindowsShell(): TPromise; public abstract setContainers(panelContainer: HTMLElement, terminalContainer: HTMLElement): void; + public abstract requestExtHostProcess(proxy: ITerminalProcessExtHostProxy, shellLaunchConfig: IShellLaunchConfig, cols: number, rows: number): void; private _restoreTabs(): void { if (!this.configHelper.config.experimentalRestore) { @@ -93,7 +93,7 @@ export abstract class TerminalService implements ITerminalService { } tabConfigs.forEach(tabConfig => { - const instance = this.createInstance(tabConfig.instances[0]); + const instance = this.createTerminal(tabConfig.instances[0]); for (let i = 1; i < tabConfig.instances.length; i++) { this.splitInstance(instance, tabConfig.instances[i]); } diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminal.contribution.ts b/src/vs/workbench/parts/terminal/electron-browser/terminal.contribution.ts index 036f94033e1..d148d55861b 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/terminal.contribution.ts +++ b/src/vs/workbench/parts/terminal/electron-browser/terminal.contribution.ts @@ -11,14 +11,14 @@ import * as debugActions from 'vs/workbench/parts/debug/browser/debugActions'; import * as nls from 'vs/nls'; import * as panel from 'vs/workbench/browser/panel'; import * as platform from 'vs/base/common/platform'; -import * as terminalCommands from 'vs/workbench/parts/terminal/electron-browser/terminalCommands'; +import * as terminalCommands from 'vs/workbench/parts/terminal/common/terminalCommands'; import { Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; -import { ITerminalService, KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_INPUT_FOCUSED, KEYBINDING_CONTEXT_TERMINAL_TEXT_SELECTED, TERMINAL_PANEL_ID, KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_VISIBLE, TerminalCursorStyle } from 'vs/workbench/parts/terminal/common/terminal'; -import { getTerminalDefaultShellUnixLike, getTerminalDefaultShellWindows } from 'vs/workbench/parts/terminal/electron-browser/terminal'; +import { ITerminalService, KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_INPUT_FOCUSED, KEYBINDING_CONTEXT_TERMINAL_TEXT_SELECTED, TERMINAL_PANEL_ID, KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_VISIBLE, TerminalCursorStyle, KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_NOT_VISIBLE } from 'vs/workbench/parts/terminal/common/terminal'; +import { getTerminalDefaultShellUnixLike, getTerminalDefaultShellWindows } from 'vs/workbench/parts/terminal/node/terminal'; import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { KillTerminalAction, CopyTerminalSelectionAction, CreateNewTerminalAction, CreateNewInActiveWorkspaceTerminalAction, FocusActiveTerminalAction, FocusNextTerminalAction, FocusPreviousTerminalAction, SelectDefaultShellWindowsTerminalAction, RunSelectedTextInTerminalAction, RunActiveFileInTerminalAction, ScrollDownTerminalAction, ScrollDownPageTerminalAction, ScrollToBottomTerminalAction, ScrollUpTerminalAction, ScrollUpPageTerminalAction, ScrollToTopTerminalAction, TerminalPasteAction, ToggleTerminalAction, ClearTerminalAction, AllowWorkspaceShellTerminalCommand, DisallowWorkspaceShellTerminalCommand, RenameTerminalAction, SelectAllTerminalAction, FocusTerminalFindWidgetAction, HideTerminalFindWidgetAction, ShowNextFindTermTerminalFindWidgetAction, ShowPreviousFindTermTerminalFindWidgetAction, DeleteWordLeftTerminalAction, DeleteWordRightTerminalAction, QuickOpenActionTermContributor, QuickOpenTermAction, TERMINAL_PICKER_PREFIX, MoveToLineStartTerminalAction, MoveToLineEndTerminalAction, SplitTerminalAction, FocusPreviousPaneTerminalAction, FocusNextPaneTerminalAction, ResizePaneLeftTerminalAction, ResizePaneRightTerminalAction, ResizePaneUpTerminalAction, ResizePaneDownTerminalAction, FocusPreviousCommandAction, FocusNextCommandAction, SelectToPreviousCommandAction, SelectToNextCommandAction } from 'vs/workbench/parts/terminal/electron-browser/terminalActions'; +import { KillTerminalAction, ClearSelectionTerminalAction, CopyTerminalSelectionAction, CreateNewTerminalAction, CreateNewInActiveWorkspaceTerminalAction, FocusActiveTerminalAction, FocusNextTerminalAction, FocusPreviousTerminalAction, SelectDefaultShellWindowsTerminalAction, RunSelectedTextInTerminalAction, RunActiveFileInTerminalAction, ScrollDownTerminalAction, ScrollDownPageTerminalAction, ScrollToBottomTerminalAction, ScrollUpTerminalAction, ScrollUpPageTerminalAction, ScrollToTopTerminalAction, TerminalPasteAction, ToggleTerminalAction, ClearTerminalAction, AllowWorkspaceShellTerminalCommand, DisallowWorkspaceShellTerminalCommand, RenameTerminalAction, SelectAllTerminalAction, FocusTerminalFindWidgetAction, HideTerminalFindWidgetAction, ShowNextFindTermTerminalFindWidgetAction, ShowPreviousFindTermTerminalFindWidgetAction, DeleteWordLeftTerminalAction, DeleteWordRightTerminalAction, QuickOpenActionTermContributor, QuickOpenTermAction, TERMINAL_PICKER_PREFIX, MoveToLineStartTerminalAction, MoveToLineEndTerminalAction, SplitTerminalAction, SplitInActiveWorkspaceTerminalAction, FocusPreviousPaneTerminalAction, FocusNextPaneTerminalAction, ResizePaneLeftTerminalAction, ResizePaneRightTerminalAction, ResizePaneUpTerminalAction, ResizePaneDownTerminalAction, ScrollToPreviousCommandAction, ScrollToNextCommandAction, SelectToPreviousCommandAction, SelectToNextCommandAction } from 'vs/workbench/parts/terminal/electron-browser/terminalActions'; import { Registry } from 'vs/platform/registry/common/platform'; import { ShowAllCommandsAction } from 'vs/workbench/parts/quickopen/browser/commandsHandler'; import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; @@ -28,7 +28,7 @@ import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { OpenNextRecentlyUsedEditorInGroupAction, OpenPreviousRecentlyUsedEditorInGroupAction, FocusActiveGroupAction, FocusFirstGroupAction, FocusSecondGroupAction, FocusThirdGroupAction } from 'vs/workbench/browser/parts/editor/editorActions'; import { EDITOR_FONT_DEFAULTS } from 'vs/editor/common/config/editorOptions'; -import { registerColors } from './terminalColorRegistry'; +import { registerColors } from 'vs/workbench/parts/terminal/common/terminalColorRegistry'; import { NavigateUpAction, NavigateDownAction, NavigateLeftAction, NavigateRightAction } from 'vs/workbench/electron-browser/actions'; import { QUICKOPEN_ACTION_ID, getQuickNavigateHandler, QUICKOPEN_FOCUS_SECONDARY_ACTION_ID } from 'vs/workbench/browser/parts/quickopen/quickopen'; import { IQuickOpenRegistry, Extensions as QuickOpenExtensions, QuickOpenHandlerDescriptor } from 'vs/workbench/browser/quickopen'; @@ -244,6 +244,7 @@ configurationRegistry.registerConfiguration({ ScrollUpPageTerminalAction.ID, ScrollToTopTerminalAction.ID, ClearTerminalAction.ID, + ClearSelectionTerminalAction.ID, debugActions.StartAction.ID, debugActions.StopAction.ID, debugActions.RunAction.ID, @@ -274,14 +275,15 @@ configurationRegistry.registerConfiguration({ TogglePanelAction.ID, 'workbench.action.quickOpenView', SplitTerminalAction.ID, + SplitInActiveWorkspaceTerminalAction.ID, FocusPreviousPaneTerminalAction.ID, FocusNextPaneTerminalAction.ID, ResizePaneLeftTerminalAction.ID, ResizePaneRightTerminalAction.ID, ResizePaneUpTerminalAction.ID, ResizePaneDownTerminalAction.ID, - FocusPreviousCommandAction.ID, - FocusNextCommandAction.ID, + ScrollToPreviousCommandAction.ID, + ScrollToNextCommandAction.ID, SelectToPreviousCommandAction.ID, SelectToNextCommandAction.ID ].sort() @@ -346,6 +348,10 @@ actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(CreateNewTermina primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.US_BACKTICK, mac: { primary: KeyMod.WinCtrl | KeyMod.Shift | KeyCode.US_BACKTICK } }), 'Terminal: Create New Integrated Terminal', category); +actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ClearSelectionTerminalAction, ClearSelectionTerminalAction.ID, ClearSelectionTerminalAction.LABEL, { + primary: KeyCode.Escape, + linux: { primary: KeyCode.Escape } +}, ContextKeyExpr.and(KEYBINDING_CONTEXT_TERMINAL_TEXT_SELECTED, KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_NOT_VISIBLE)), 'Terminal: Escape selection', category); actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(CreateNewInActiveWorkspaceTerminalAction, CreateNewInActiveWorkspaceTerminalAction.ID, CreateNewInActiveWorkspaceTerminalAction.LABEL), 'Terminal: Create New Integrated Terminal (In Active Workspace)', category); actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(FocusActiveTerminalAction, FocusActiveTerminalAction.ID, FocusActiveTerminalAction.LABEL), 'Terminal: Focus Terminal', category); actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(FocusNextTerminalAction, FocusNextTerminalAction.ID, FocusNextTerminalAction.LABEL), 'Terminal: Focus Next Terminal', category); @@ -372,7 +378,7 @@ actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ToggleTerminalAc mac: { primary: KeyMod.WinCtrl | KeyCode.US_BACKTICK } }), 'View: Toggle Integrated Terminal', nls.localize('viewCategory', "View")); actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ScrollDownTerminalAction, ScrollDownTerminalAction.ID, ScrollDownTerminalAction.LABEL, { - primary: KeyMod.CtrlCmd | KeyCode.DownArrow, + primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.PageDown, linux: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.DownArrow } }, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Scroll Down (Line)', category); actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ScrollDownPageTerminalAction, ScrollDownPageTerminalAction.ID, ScrollDownPageTerminalAction.LABEL, { @@ -384,7 +390,7 @@ actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ScrollToBottomTe linux: { primary: KeyMod.Shift | KeyCode.End } }, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Scroll to Bottom', category); actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ScrollUpTerminalAction, ScrollUpTerminalAction.ID, ScrollUpTerminalAction.LABEL, { - primary: KeyMod.CtrlCmd | KeyCode.UpArrow, + primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.PageUp, linux: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.UpArrow }, }, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Scroll Up (Line)', category); actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ScrollUpPageTerminalAction, ScrollUpPageTerminalAction.ID, ScrollUpPageTerminalAction.LABEL, { @@ -442,6 +448,7 @@ actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(SplitTerminalAct secondary: [KeyMod.WinCtrl | KeyMod.Shift | KeyCode.KEY_5] } }, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Split', category); +actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(SplitInActiveWorkspaceTerminalAction, SplitInActiveWorkspaceTerminalAction.ID, SplitInActiveWorkspaceTerminalAction.LABEL), 'Terminal: Split Terminal (In Active Workspace)', category); actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(FocusPreviousPaneTerminalAction, FocusPreviousPaneTerminalAction.ID, FocusPreviousPaneTerminalAction.LABEL, { primary: KeyMod.Alt | KeyCode.LeftArrow, secondary: [KeyMod.Alt | KeyCode.UpArrow], @@ -478,14 +485,14 @@ actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ResizePaneDownTe linux: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.DownArrow }, mac: { primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.DownArrow } }, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Resize Pane Down', category); -actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(FocusPreviousCommandAction, FocusPreviousCommandAction.ID, FocusPreviousCommandAction.LABEL, { +actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ScrollToPreviousCommandAction, ScrollToPreviousCommandAction.ID, ScrollToPreviousCommandAction.LABEL, { primary: null, mac: { primary: KeyMod.CtrlCmd | KeyCode.UpArrow } -}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Focus Previous Command', category); -actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(FocusNextCommandAction, FocusNextCommandAction.ID, FocusNextCommandAction.LABEL, { +}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Scroll To Previous Command', category); +actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ScrollToNextCommandAction, ScrollToNextCommandAction.ID, ScrollToNextCommandAction.LABEL, { primary: null, mac: { primary: KeyMod.CtrlCmd | KeyCode.DownArrow } -}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Focus Next Command', category); +}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Scroll To Next Command', category); actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(SelectToPreviousCommandAction, SelectToPreviousCommandAction.ID, SelectToPreviousCommandAction.LABEL, { primary: null, mac: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.UpArrow } diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminalActions.ts b/src/vs/workbench/parts/terminal/electron-browser/terminalActions.ts index 42065e46e6e..6ceee359222 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/terminalActions.ts +++ b/src/vs/workbench/parts/terminal/electron-browser/terminalActions.ts @@ -46,7 +46,7 @@ export class ToggleTerminalAction extends TogglePanelAction { if (this.terminalService.terminalInstances.length === 0) { // If there is not yet an instance attempt to create it here so that we can suggest a // new shell on Windows (and not do so when the panel is restored on reload). - const newTerminalInstance = this.terminalService.createInstance(undefined, true); + const newTerminalInstance = this.terminalService.createTerminal(undefined, true); const toDispose = newTerminalInstance.onProcessIdReady(() => { newTerminalInstance.focus(); toDispose.dispose(); @@ -258,7 +258,7 @@ export class CreateNewTerminalAction extends Action { if (folders.length <= 1) { // Allow terminal service to handle the path when there is only a // single root - instancePromise = TPromise.as(this.terminalService.createInstance(undefined, true)); + instancePromise = TPromise.as(this.terminalService.createTerminal(undefined, true)); } else { const options: IPickOptions = { placeHolder: nls.localize('workbench.action.terminal.newWorkspacePlaceholder', "Select current working directory for new terminal") @@ -268,7 +268,7 @@ export class CreateNewTerminalAction extends Action { // Don't create the instance if the workspace picker was canceled return null; } - return this.terminalService.createInstance({ cwd: workspace.uri.fsPath }, true); + return this.terminalService.createTerminal({ cwd: workspace.uri.fsPath }, true); }); } @@ -295,7 +295,7 @@ export class CreateNewInActiveWorkspaceTerminalAction extends Action { } public run(event?: any): TPromise { - const instance = this.terminalService.createInstance(undefined, true); + const instance = this.terminalService.createTerminal(undefined, true); if (!instance) { return TPromise.as(void 0); } @@ -310,11 +310,57 @@ export class SplitTerminalAction extends Action { constructor( id: string, label: string, - @ITerminalService private readonly _terminalService: ITerminalService + @ITerminalService private readonly _terminalService: ITerminalService, + @ICommandService private commandService: ICommandService, + @IWorkspaceContextService private workspaceContextService: IWorkspaceContextService ) { super(id, label, 'terminal-action split'); } + public run(event?: any): TPromise { + const instance = this._terminalService.getActiveInstance(); + if (!instance) { + return TPromise.as(void 0); + } + + const folders = this.workspaceContextService.getWorkspace().folders; + + let pathPromise: TPromise = TPromise.as({}); + if (folders.length > 1) { + // Only choose a path when there's more than 1 folder + const options: IPickOptions = { + placeHolder: nls.localize('workbench.action.terminal.newWorkspacePlaceholder', "Select current working directory for new terminal") + }; + pathPromise = this.commandService.executeCommand(PICK_WORKSPACE_FOLDER_COMMAND_ID, [options]).then(workspace => { + if (!workspace) { + // Don't split the instance if the workspace picker was canceled + return null; + } + return TPromise.as({ cwd: workspace.uri.fsPath }); + }); + } + + return pathPromise.then(path => { + if (!path) { + return TPromise.as(void 0); + } + this._terminalService.splitInstance(instance, path); + return this._terminalService.showPanel(true); + }); + } +} + +export class SplitInActiveWorkspaceTerminalAction extends Action { + public static readonly ID = 'workbench.action.terminal.splitInActiveWorkspace'; + public static readonly LABEL = nls.localize('workbench.action.terminal.splitInActiveWorkspace', "Split Terminal (In Active Workspace)"); + + constructor( + id: string, label: string, + @ITerminalService private readonly _terminalService: ITerminalService + ) { + super(id, label); + } + public run(event?: any): TPromise { const instance = this._terminalService.getActiveInstance(); if (!instance) { @@ -787,6 +833,27 @@ export class ClearTerminalAction extends Action { } } +export class ClearSelectionTerminalAction extends Action { + + public static readonly ID = 'workbench.action.terminal.clearSelection'; + public static readonly LABEL = nls.localize('workbench.action.terminal.clearSelection', "Clear Selection"); + + constructor( + id: string, label: string, + @ITerminalService private terminalService: ITerminalService + ) { + super(id, label); + } + + public run(event?: any): TPromise { + let terminalInstance = this.terminalService.getActiveInstance(); + if (terminalInstance && terminalInstance.hasSelection()) { + terminalInstance.clearSelection(); + } + return TPromise.as(void 0); + } +} + export class AllowWorkspaceShellTerminalCommand extends Action { public static readonly ID = 'workbench.action.terminal.allowWorkspaceShell'; @@ -982,9 +1049,9 @@ export class RenameTerminalQuickOpenAction extends RenameTerminalAction { } } -export class FocusPreviousCommandAction extends Action { - public static readonly ID = 'workbench.action.terminal.focusPreviousCommand'; - public static readonly LABEL = nls.localize('workbench.action.terminal.focusPreviousCommand', "Focus Previous Command"); +export class ScrollToPreviousCommandAction extends Action { + public static readonly ID = 'workbench.action.terminal.scrollToPreviousCommand'; + public static readonly LABEL = nls.localize('workbench.action.terminal.scrollToPreviousCommand', "Scroll To Previous Command"); constructor( id: string, label: string, @@ -996,15 +1063,16 @@ export class FocusPreviousCommandAction extends Action { public run(): TPromise { const instance = this.terminalService.getActiveInstance(); if (instance) { - instance.commandTracker.focusPreviousCommand(); + instance.commandTracker.scrollToPreviousCommand(); + instance.focus(); } return TPromise.as(void 0); } } -export class FocusNextCommandAction extends Action { - public static readonly ID = 'workbench.action.terminal.focusNextCommand'; - public static readonly LABEL = nls.localize('workbench.action.terminal.focusNextCommand', "Focus Next Command"); +export class ScrollToNextCommandAction extends Action { + public static readonly ID = 'workbench.action.terminal.scrollToNextCommand'; + public static readonly LABEL = nls.localize('workbench.action.terminal.scrollToNextCommand', "Scroll To Next Command"); constructor( id: string, label: string, @@ -1016,7 +1084,8 @@ export class FocusNextCommandAction extends Action { public run(): TPromise { const instance = this.terminalService.getActiveInstance(); if (instance) { - instance.commandTracker.focusNextCommand(); + instance.commandTracker.scrollToNextCommand(); + instance.focus(); } return TPromise.as(void 0); } @@ -1037,6 +1106,7 @@ export class SelectToPreviousCommandAction extends Action { const instance = this.terminalService.getActiveInstance(); if (instance) { instance.commandTracker.selectToPreviousCommand(); + instance.focus(); } return TPromise.as(void 0); } @@ -1057,6 +1127,7 @@ export class SelectToNextCommandAction extends Action { const instance = this.terminalService.getActiveInstance(); if (instance) { instance.commandTracker.selectToNextCommand(); + instance.focus(); } return TPromise.as(void 0); } diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminalConfigHelper.ts b/src/vs/workbench/parts/terminal/electron-browser/terminalConfigHelper.ts index 314511a4e90..a8e3f9ecbae 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/terminalConfigHelper.ts +++ b/src/vs/workbench/parts/terminal/electron-browser/terminalConfigHelper.ts @@ -12,7 +12,7 @@ import { IWorkspaceConfigurationService } from 'vs/workbench/services/configurat import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { ITerminalConfiguration, ITerminalConfigHelper, ITerminalFont, IShellLaunchConfig, IS_WORKSPACE_SHELL_ALLOWED_STORAGE_KEY, TERMINAL_CONFIG_SECTION } from 'vs/workbench/parts/terminal/common/terminal'; import Severity from 'vs/base/common/severity'; -import { isFedora } from 'vs/workbench/parts/terminal/electron-browser/terminal'; +import { isFedora } from 'vs/workbench/parts/terminal/node/terminal'; import { Terminal as XTermTerminal } from 'vscode-xterm'; import { INotificationService } from 'vs/platform/notification/common/notification'; @@ -163,18 +163,16 @@ export class TerminalConfigHelper implements ITerminalConfigHelper { } else { // if (shellArgsConfigValue.workspace !== undefined) changeString = `shellArgs: ${argsString}`; } - const message = nls.localize('terminal.integrated.allowWorkspaceShell', "Do you allow {0} (defined as a workspace setting) to be launched in the terminal?", changeString); - const options = [nls.localize('allow', "Allow"), nls.localize('disallow', "Disallow")]; - this._notificationService.prompt(Severity.Info, message, options).then(choice => { - switch (choice) { - case 0: /* Allow */ - this._storageService.store(IS_WORKSPACE_SHELL_ALLOWED_STORAGE_KEY, true, StorageScope.WORKSPACE); - break; - case 1: /* Disallow */ - this._storageService.store(IS_WORKSPACE_SHELL_ALLOWED_STORAGE_KEY, false, StorageScope.WORKSPACE); - break; - } - }); + this._notificationService.prompt(Severity.Info, nls.localize('terminal.integrated.allowWorkspaceShell', "Do you allow {0} (defined as a workspace setting) to be launched in the terminal?", changeString), + [{ + label: nls.localize('allow', "Allow"), + run: () => this._storageService.store(IS_WORKSPACE_SHELL_ALLOWED_STORAGE_KEY, true, StorageScope.WORKSPACE) + }, + { + label: nls.localize('disallow', "Disallow"), + run: () => this._storageService.store(IS_WORKSPACE_SHELL_ALLOWED_STORAGE_KEY, false, StorageScope.WORKSPACE) + }] + ); } shell.executable = (isWorkspaceShellAllowed ? shellConfigValue.value : shellConfigValue.user) || shellConfigValue.default; diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminalInstance.ts b/src/vs/workbench/parts/terminal/electron-browser/terminalInstance.ts index 21ec8a5ea72..0459ce481c8 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/terminalInstance.ts +++ b/src/vs/workbench/parts/terminal/electron-browser/terminalInstance.ts @@ -3,23 +3,18 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as cp from 'child_process'; -import * as os from 'os'; -import * as path from 'path'; import * as lifecycle from 'vs/base/common/lifecycle'; import * as nls from 'vs/nls'; import * as platform from 'vs/base/common/platform'; import * as dom from 'vs/base/browser/dom'; +import * as paths from 'vs/base/common/paths'; import { Event, Emitter } from 'vs/base/common/event'; -import Uri from 'vs/base/common/uri'; -import { WindowsShellHelper } from 'vs/workbench/parts/terminal/electron-browser/windowsShellHelper'; +import { WindowsShellHelper } from 'vs/workbench/parts/terminal/node/windowsShellHelper'; import { Terminal as XTermTerminal } from 'vscode-xterm'; -import { Dimension } from 'vs/base/browser/builder'; import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; -import { IStringDictionary } from 'vs/base/common/collections'; -import { ITerminalInstance, KEYBINDING_CONTEXT_TERMINAL_TEXT_SELECTED, TERMINAL_PANEL_ID, IShellLaunchConfig } from 'vs/workbench/parts/terminal/common/terminal'; +import { ITerminalInstance, KEYBINDING_CONTEXT_TERMINAL_TEXT_SELECTED, TERMINAL_PANEL_ID, IShellLaunchConfig, ITerminalProcessManager, ProcessState } from 'vs/workbench/parts/terminal/common/terminal'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { TabFocus } from 'vs/editor/common/config/commonEditorConfig'; @@ -30,155 +25,110 @@ import { registerThemingParticipant, ITheme, ICssStyleCollector, IThemeService } import { scrollbarSliderBackground, scrollbarSliderHoverBackground, scrollbarSliderActiveBackground, activeContrastBorder } from 'vs/platform/theme/common/colorRegistry'; import { TPromise } from 'vs/base/common/winjs.base'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; -import { IHistoryService } from 'vs/workbench/services/history/common/history'; -import pkg from 'vs/platform/node/package'; -import { ansiColorIdentifiers, TERMINAL_BACKGROUND_COLOR, TERMINAL_FOREGROUND_COLOR, TERMINAL_CURSOR_FOREGROUND_COLOR, TERMINAL_CURSOR_BACKGROUND_COLOR, TERMINAL_SELECTION_BACKGROUND_COLOR } from 'vs/workbench/parts/terminal/electron-browser/terminalColorRegistry'; +import { ansiColorIdentifiers, TERMINAL_BACKGROUND_COLOR, TERMINAL_FOREGROUND_COLOR, TERMINAL_CURSOR_FOREGROUND_COLOR, TERMINAL_CURSOR_BACKGROUND_COLOR, TERMINAL_SELECTION_BACKGROUND_COLOR } from 'vs/workbench/parts/terminal/common/terminalColorRegistry'; import { PANEL_BACKGROUND } from 'vs/workbench/common/theme'; -import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; -import { IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { ILogService } from 'vs/platform/log/common/log'; import { TerminalCommandTracker } from 'vs/workbench/parts/terminal/node/terminalCommandTracker'; - -/** The amount of time to consider terminal errors to be related to the launch */ -const LAUNCHING_DURATION = 500; +import { TerminalProcessManager } from './terminalProcessManager'; let Terminal: typeof XTermTerminal; -enum ProcessState { - // The process has not been initialized yet. - UNINITIALIZED, - // The process is currently launching, the process is marked as launching - // for a short duration after being created and is helpful to indicate - // whether the process died as a result of bad shell and args. - LAUNCHING, - // The process is running normally. - RUNNING, - // The process was killed during launch, likely as a result of bad shell and - // args. - KILLED_DURING_LAUNCH, - // The process was killed by the user (the event originated from VS Code). - KILLED_BY_USER, - // The process was killed by itself, for example the shell crashed or `exit` - // was run. - KILLED_BY_PROCESS -} - export class TerminalInstance implements ITerminalInstance { private static readonly EOL_REGEX = /\r?\n/g; - private static _lastKnownDimensions: Dimension = null; + private static _lastKnownDimensions: dom.Dimension = null; private static _idCounter = 1; + private _processManager: ITerminalProcessManager | undefined; + private _id: number; private _isExiting: boolean; private _hadFocusOnExit: boolean; private _isVisible: boolean; - private _processState: ProcessState; - private _processReady: TPromise; private _isDisposed: boolean; - private readonly _onDisposed: Emitter; - private readonly _onFocused: Emitter; - private readonly _onProcessIdReady: Emitter; - private readonly _onTitleChanged: Emitter; - private _process: cp.ChildProcess; - private _processId: number; private _skipTerminalCommands: string[]; private _title: string; - private _instanceDisposables: lifecycle.IDisposable[]; - private _processDisposables: lifecycle.IDisposable[]; private _wrapperElement: HTMLDivElement; private _xterm: XTermTerminal; private _xtermElement: HTMLDivElement; private _terminalHasTextContextKey: IContextKey; private _cols: number; private _rows: number; - private _messageTitleListener: (message: { type: string, content: string }) => void; - private _preLaunchInputQueue: string; - private _initialCwd: string; private _windowsShellHelper: WindowsShellHelper; private _onLineDataListeners: ((lineData: string) => void)[]; private _xtermReadyPromise: TPromise; + private _disposables: lifecycle.IDisposable[]; + private _messageTitleDisposable: lifecycle.IDisposable; + private _widgetManager: TerminalWidgetManager; private _linkHandler: TerminalLinkHandler; private _commandTracker: TerminalCommandTracker; public disableLayout: boolean; public get id(): number { return this._id; } - public get processId(): number { return this._processId; } - public get onDisposed(): Event { return this._onDisposed.event; } - public get onFocused(): Event { return this._onFocused.event; } - public get onProcessIdReady(): Event { return this._onProcessIdReady.event; } - public get onTitleChanged(): Event { return this._onTitleChanged.event; } + // TODO: Ideally processId would be merged into processReady + public get processId(): number | undefined { return this._processManager ? this._processManager.shellProcessId : undefined; } + // TODO: How does this work with detached processes? + // TODO: Should this be an event as it can fire twice? + public get processReady(): TPromise { return this._processManager ? this._processManager.ptyProcessReady : TPromise.as(void 0); } public get title(): string { return this._title; } public get hadFocusOnExit(): boolean { return this._hadFocusOnExit; } - public get isTitleSetByProcess(): boolean { return !!this._messageTitleListener; } + public get isTitleSetByProcess(): boolean { return !!this._messageTitleDisposable; } public get shellLaunchConfig(): IShellLaunchConfig { return Object.freeze(this._shellLaunchConfig); } public get commandTracker(): TerminalCommandTracker { return this._commandTracker; } + + private readonly _onDisposed: Emitter = new Emitter(); + public get onDisposed(): Event { return this._onDisposed.event; } + private readonly _onFocused: Emitter = new Emitter(); + public get onFocused(): Event { return this._onFocused.event; } + private readonly _onProcessIdReady: Emitter = new Emitter(); + public get onProcessIdReady(): Event { return this._onProcessIdReady.event; } + private readonly _onTitleChanged: Emitter = new Emitter(); + public get onTitleChanged(): Event { return this._onTitleChanged.event; } + private readonly _onRequestExtHostProcess: Emitter = new Emitter(); + public get onRequestExtHostProcess(): Event { return this._onRequestExtHostProcess.event; } + public constructor( private _terminalFocusContextKey: IContextKey, private _configHelper: TerminalConfigHelper, private _container: HTMLElement, private _shellLaunchConfig: IShellLaunchConfig, + doCreateProcess: boolean, @IContextKeyService private readonly _contextKeyService: IContextKeyService, @IKeybindingService private readonly _keybindingService: IKeybindingService, @INotificationService private readonly _notificationService: INotificationService, @IPanelService private readonly _panelService: IPanelService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @IClipboardService private readonly _clipboardService: IClipboardService, - @IHistoryService private readonly _historyService: IHistoryService, @IThemeService private readonly _themeService: IThemeService, - @IConfigurationResolverService private readonly _configurationResolverService: IConfigurationResolverService, - @IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService, @IConfigurationService private readonly _configurationService: IConfigurationService, @ILogService private _logService: ILogService ) { - this._instanceDisposables = []; - this._processDisposables = []; + this._disposables = []; this._skipTerminalCommands = []; this._onLineDataListeners = []; this._isExiting = false; this._hadFocusOnExit = false; - this._processState = ProcessState.UNINITIALIZED; this._isVisible = false; this._isDisposed = false; this._id = TerminalInstance._idCounter++; this._terminalHasTextContextKey = KEYBINDING_CONTEXT_TERMINAL_TEXT_SELECTED.bindTo(this._contextKeyService); - this._preLaunchInputQueue = ''; this.disableLayout = false; this._logService.trace(`terminalInstance#ctor (id: ${this.id})`, this._shellLaunchConfig); - this._onDisposed = new Emitter(); - this._onFocused = new Emitter(); - this._onProcessIdReady = new Emitter(); - this._onTitleChanged = new Emitter(); - - // Create a promise that resolves when the pty is ready - this._processReady = new TPromise(c => { - this.onProcessIdReady(() => { - this._logService.debug(`Terminal process ready (id: ${this.id}, processId: ${this.processId})`); - c(void 0); - }); - }); - this._initDimensions(); - this._createProcess(); + if (doCreateProcess) { + this._createProcess(); + } this._xtermReadyPromise = this._createXterm(); this._xtermReadyPromise.then(() => { - if (platform.isWindows) { - this._processReady.then(() => { - if (!this._isDisposed) { - this._windowsShellHelper = new WindowsShellHelper(this._processId, this, this._xterm); - } - }); - } - // Only attach xterm.js to the DOM if the terminal panel has been opened before. if (_container) { this._attachToElement(_container); @@ -196,7 +146,7 @@ export class TerminalInstance implements ITerminalInstance { } public addDisposable(disposable: lifecycle.IDisposable): void { - this._instanceDisposables.push(disposable); + this._disposables.push(disposable); } private _initDimensions(): void { @@ -246,7 +196,7 @@ export class TerminalInstance implements ITerminalInstance { return dimension.width; } - private _getDimension(width: number, height: number): Dimension { + private _getDimension(width: number, height: number): dom.Dimension { // The font needs to have been initialized const font = this._configHelper.getFont(this._xterm); if (!font || !font.charWidth || !font.charHeight) { @@ -278,7 +228,7 @@ export class TerminalInstance implements ITerminalInstance { const innerWidth = width - marginLeft - marginRight; const innerHeight = height - bottom; - TerminalInstance._lastKnownDimensions = new Dimension(innerWidth, innerHeight); + TerminalInstance._lastKnownDimensions = new dom.Dimension(innerWidth, innerHeight); return TerminalInstance._lastKnownDimensions; } @@ -317,24 +267,14 @@ export class TerminalInstance implements ITerminalInstance { } this._xterm.winptyCompatInit(); this._xterm.on('linefeed', () => this._onLineFeed()); - this._process.on('message', (message) => this._sendPtyDataToXterm(message)); - this._xterm.on('data', (data) => { - if (this._processId) { - // Send data if the pty is ready - this._process.send({ - event: 'input', - data - }); - } else { - // If the pty is not ready, queue the data received from - // xterm.js until the pty is ready - this._preLaunchInputQueue += data; - } - return false; - }); - this._linkHandler = this._instantiationService.createInstance(TerminalLinkHandler, this._xterm, platform.platform, this._initialCwd); + if (this._processManager) { + this._processManager.onProcessData(data => this._sendPtyDataToXterm(data)); + this._xterm.on('data', data => this._processManager.write(data)); + // TODO: How does the cwd work on detached processes? + this._linkHandler = this._instantiationService.createInstance(TerminalLinkHandler, this._xterm, platform.platform, this._processManager.initialCwd); + } this._commandTracker = new TerminalCommandTracker(this._xterm); - this._instanceDisposables.push(this._themeService.onThemeChange(theme => this._updateTheme(theme))); + this._disposables.push(this._themeService.onThemeChange(theme => this._updateTheme(theme))); } public reattachToElement(container: HTMLElement): void { @@ -361,7 +301,6 @@ export class TerminalInstance implements ITerminalInstance { return; } - // TODO: Verify listeners still work // The container changed, reattach this._container.removeChild(this._wrapperElement); this._container = container; @@ -410,7 +349,7 @@ export class TerminalInstance implements ITerminalInstance { return undefined; }); - this._instanceDisposables.push(dom.addDisposableListener(this._xterm.element, 'mousedown', (event: KeyboardEvent) => { + this._disposables.push(dom.addDisposableListener(this._xterm.element, 'mousedown', (event: KeyboardEvent) => { // We need to listen to the mouseup event on the document since the user may release // the mouse button anywhere outside of _xterm.element. const listener = dom.addDisposableListener(document, 'mouseup', (event: KeyboardEvent) => { @@ -422,7 +361,7 @@ export class TerminalInstance implements ITerminalInstance { })); // xterm.js currently drops selection on keyup as we need to handle this case. - this._instanceDisposables.push(dom.addDisposableListener(this._xterm.element, 'keyup', (event: KeyboardEvent) => { + this._disposables.push(dom.addDisposableListener(this._xterm.element, 'keyup', (event: KeyboardEvent) => { // Wait until keyup has propagated through the DOM before evaluating // the new selection state. setTimeout(() => this._refreshSelectionContextKey(), 0); @@ -432,7 +371,7 @@ export class TerminalInstance implements ITerminalInstance { const focusTrap: HTMLElement = document.createElement('div'); focusTrap.setAttribute('tabindex', '0'); dom.addClass(focusTrap, 'focus-trap'); - this._instanceDisposables.push(dom.addDisposableListener(focusTrap, 'focus', (event: FocusEvent) => { + this._disposables.push(dom.addDisposableListener(focusTrap, 'focus', (event: FocusEvent) => { let currentElement = focusTrap; while (!dom.hasClass(currentElement, 'part')) { currentElement = currentElement.parentElement; @@ -442,31 +381,34 @@ export class TerminalInstance implements ITerminalInstance { })); xtermHelper.insertBefore(focusTrap, this._xterm.textarea); - this._instanceDisposables.push(dom.addDisposableListener(this._xterm.textarea, 'focus', (event: KeyboardEvent) => { + this._disposables.push(dom.addDisposableListener(this._xterm.textarea, 'focus', (event: KeyboardEvent) => { this._terminalFocusContextKey.set(true); this._onFocused.fire(this); })); - this._instanceDisposables.push(dom.addDisposableListener(this._xterm.textarea, 'blur', (event: KeyboardEvent) => { + this._disposables.push(dom.addDisposableListener(this._xterm.textarea, 'blur', (event: KeyboardEvent) => { this._terminalFocusContextKey.reset(); this._refreshSelectionContextKey(); })); - this._instanceDisposables.push(dom.addDisposableListener(this._xterm.element, 'focus', (event: KeyboardEvent) => { + this._disposables.push(dom.addDisposableListener(this._xterm.element, 'focus', (event: KeyboardEvent) => { this._terminalFocusContextKey.set(true); })); - this._instanceDisposables.push(dom.addDisposableListener(this._xterm.element, 'blur', (event: KeyboardEvent) => { + this._disposables.push(dom.addDisposableListener(this._xterm.element, 'blur', (event: KeyboardEvent) => { this._terminalFocusContextKey.reset(); this._refreshSelectionContextKey(); })); this._wrapperElement.appendChild(this._xtermElement); - this._widgetManager = new TerminalWidgetManager(this._wrapperElement); - this._linkHandler.setWidgetManager(this._widgetManager); this._container.appendChild(this._wrapperElement); + if (this._processManager) { + this._widgetManager = new TerminalWidgetManager(this._wrapperElement); + this._linkHandler.setWidgetManager(this._widgetManager); + } + const computedStyle = window.getComputedStyle(this._container); const width = parseInt(computedStyle.getPropertyValue('width').replace('px', ''), 10); const height = parseInt(computedStyle.getPropertyValue('height').replace('px', ''), 10); - this.layout(new Dimension(width, height)); + this.layout(new dom.Dimension(width, height)); this.setVisible(this._isVisible); this.updateConfig(); @@ -498,7 +440,7 @@ export class TerminalInstance implements ITerminalInstance { } } - get selection(): string | undefined { + public get selection(): string | undefined { return this.hasSelection() ? this._xterm.getSelection() : undefined; } @@ -547,22 +489,14 @@ export class TerminalInstance implements ITerminalInstance { this._xterm.destroy(); this._xterm = null; } - if (this._process) { - if (this._process.connected) { - // If the process was still connected this dispose came from - // within VS Code, not the process, so mark the process as - // killed by the user. - this._processState = ProcessState.KILLED_BY_USER; - this._process.send({ event: 'shutdown' }); - } - this._process = null; + if (this._processManager) { + this._processManager.dispose(); } if (!this._isDisposed) { this._isDisposed = true; this._onDisposed.fire(this); } - this._processDisposables = lifecycle.dispose(this._processDisposables); - this._instanceDisposables = lifecycle.dispose(this._instanceDisposables); + this._disposables = lifecycle.dispose(this._disposables); } public focus(force?: boolean): void { @@ -581,16 +515,13 @@ export class TerminalInstance implements ITerminalInstance { } public sendText(text: string, addNewLine: boolean): void { - this._processReady.then(() => { + this._processManager.ptyProcessReady.then(() => { // Normalize line endings to 'enter' press. text = text.replace(TerminalInstance.EOL_REGEX, '\r'); if (addNewLine && text.substr(text.length - 1) !== '\r') { text += '\r'; } - this._process.send({ - event: 'input', - data: text - }); + this._processManager.write(text); }); } @@ -613,7 +544,7 @@ export class TerminalInstance implements ITerminalInstance { const computedStyle = window.getComputedStyle(this._container); const width = parseInt(computedStyle.getPropertyValue('width').replace('px', ''), 10); const height = parseInt(computedStyle.getPropertyValue('height').replace('px', ''), 10); - this.layout(new Dimension(width, height)); + this.layout(new dom.Dimension(width, height)); } } } @@ -643,7 +574,13 @@ export class TerminalInstance implements ITerminalInstance { } public clear(): void { - this._xterm.clear(); + if (this._shellLaunchConfig.executable && paths.basename(this._shellLaunchConfig.executable).match(/^(zsh|bash|bash\.exe)$/)) { + // If a supported shell is being used, clear xterm scrollback then clear shell (^L) + this._xterm.write('\x1b[3J'); + this._processManager.write('\x0c'); + } else { + this._xterm.clear(); + } } private _refreshSelectionContextKey() { @@ -653,149 +590,60 @@ export class TerminalInstance implements ITerminalInstance { this._terminalHasTextContextKey.set(isActive && this.hasSelection()); } - protected _getCwd(shell: IShellLaunchConfig, root: Uri): string { - if (shell.cwd) { - return shell.cwd; - } - - let cwd: string; - - // TODO: Handle non-existent customCwd - if (!shell.ignoreConfigurationCwd) { - // Evaluate custom cwd first - const customCwd = this._configHelper.config.cwd; - if (customCwd) { - if (path.isAbsolute(customCwd)) { - cwd = customCwd; - } else if (root) { - cwd = path.normalize(path.join(root.fsPath, customCwd)); - } - } - } - - // If there was no custom cwd or it was relative with no workspace - if (!cwd) { - cwd = root ? root.fsPath : os.homedir(); - } - - return TerminalInstance._sanitizeCwd(cwd); - } - protected _createProcess(): void { - const locale = this._configHelper.config.setLocaleVariables ? platform.locale : undefined; - if (!this._shellLaunchConfig.executable) { - this._configHelper.mergeDefaultShellPathAndArgs(this._shellLaunchConfig); - } - - const lastActiveWorkspaceRootUri = this._historyService.getLastActiveWorkspaceRoot('file'); - this._initialCwd = this._getCwd(this._shellLaunchConfig, lastActiveWorkspaceRootUri); - - // Resolve env vars from config and shell - const lastActiveWorkspaceRoot = this._workspaceContextService.getWorkspaceFolder(lastActiveWorkspaceRootUri); - const platformKey = platform.isWindows ? 'windows' : (platform.isMacintosh ? 'osx' : 'linux'); - const envFromConfig = TerminalInstance.resolveConfigurationVariables(this._configurationResolverService, { ...this._configHelper.config.env[platformKey] }, lastActiveWorkspaceRoot); - const envFromShell = TerminalInstance.resolveConfigurationVariables(this._configurationResolverService, { ...this._shellLaunchConfig.env }, lastActiveWorkspaceRoot); - this._shellLaunchConfig.env = envFromShell; - - // Merge process env with the env from config - const parentEnv = { ...process.env }; - TerminalInstance.mergeEnvironments(parentEnv, envFromConfig); - - // Continue env initialization, merging in the env from the launch - // config and adding keys that are needed to create the process - const env = TerminalInstance.createTerminalEnv(parentEnv, this._shellLaunchConfig, this._initialCwd, locale, this._cols, this._rows); - this._process = cp.fork(Uri.parse(require.toUrl('bootstrap')).fsPath, ['--type=terminal'], { - env, - cwd: Uri.parse(path.dirname(require.toUrl('../node/terminalProcess'))).fsPath - }); - this._processState = ProcessState.LAUNCHING; + this._processManager = this._instantiationService.createInstance(TerminalProcessManager, this._id, this._configHelper); + this._processManager.onProcessReady(() => this._onProcessIdReady.fire(this)); + this._processManager.onProcessExit(exitCode => this._onProcessExit(exitCode)); + this._processManager.createProcess(this._shellLaunchConfig, this._cols, this._rows); if (this._shellLaunchConfig.name) { this.setTitle(this._shellLaunchConfig.name, false); } else { // Only listen for process title changes when a name is not provided this.setTitle(this._shellLaunchConfig.executable, true); - this._messageTitleListener = (message) => { - if (message.type === 'title') { - this.setTitle(message.content ? message.content : '', true); - } - }; - this._process.on('message', this._messageTitleListener); + this._messageTitleDisposable = this._processManager.onProcessTitle(title => this.setTitle(title ? title : '', true)); } - this._process.on('message', (message) => { - if (message.type === 'pid') { - this._processId = message.content; - // Send any queued data that's waiting - if (this._preLaunchInputQueue.length > 0) { - this._process.send({ - event: 'input', - data: this._preLaunchInputQueue - }); - this._preLaunchInputQueue = null; - } - this._onProcessIdReady.fire(this); - } - }); - this._process.on('exit', exitCode => this._onPtyProcessExit(exitCode)); - setTimeout(() => { - if (this._processState === ProcessState.LAUNCHING) { - this._processState = ProcessState.RUNNING; - } - }, LAUNCHING_DURATION); - } - - // TODO: Should be protected - private static resolveConfigurationVariables(configurationResolverService: IConfigurationResolverService, env: IStringDictionary, lastActiveWorkspaceRoot: IWorkspaceFolder): IStringDictionary { - Object.keys(env).forEach((key) => { - if (typeof env[key] === 'string') { - env[key] = configurationResolverService.resolve(lastActiveWorkspaceRoot, env[key]); - } - }); - return env; - } - - private _sendPtyDataToXterm(message: { type: string, content: string }): void { - if (message.type === 'data') { - if (this._widgetManager) { - this._widgetManager.closeMessage(); - } - if (this._xterm) { - this._xterm.write(message.content); - } + if (platform.isWindows) { + this._processManager.ptyProcessReady.then(() => { + this._xtermReadyPromise.then(() => { + if (!this._isDisposed) { + this._windowsShellHelper = new WindowsShellHelper(this._processManager.shellProcessId, this, this._xterm); + } + }); + }); } } - private _onPtyProcessExit(exitCode: number): void { + private _sendPtyDataToXterm(data: string): void { + if (this._widgetManager) { + this._widgetManager.closeMessage(); + } + if (this._xterm) { + this._xterm.write(data); + } + } + + private _onProcessExit(exitCode: number): void { + this._logService.debug(`Terminal process exit (id: ${this.id}) with code ${exitCode}`); + // Prevent dispose functions being triggered multiple times if (this._isExiting) { return; } this._isExiting = true; - this._process = null; let exitCodeMessage: string; if (exitCode) { exitCodeMessage = nls.localize('terminal.integrated.exitedWithCode', 'The terminal process terminated with exit code: {0}', exitCode); } - // If the process is marked as launching then mark the process as killed - // during launch. This typically means that there is a problem with the - // shell and args. - if (this._processState === ProcessState.LAUNCHING) { - this._processState = ProcessState.KILLED_DURING_LAUNCH; - } - - // If TerminalInstance did not know about the process exit then it was - // triggered by the process, not on VS Code's side. - if (this._processState === ProcessState.RUNNING) { - this._processState = ProcessState.KILLED_BY_PROCESS; - } + this._logService.debug(`Terminal process exit (id: ${this.id}) state ${this._processManager.processState}`); // Only trigger wait on exit when the exit was *not* triggered by the // user (via the `workbench.action.terminal.kill` command). - if (this._shellLaunchConfig.waitOnExit && this._processState !== ProcessState.KILLED_BY_USER) { + if (this._shellLaunchConfig.waitOnExit && this._processManager.processState !== ProcessState.KILLED_BY_USER) { if (exitCode) { this._xterm.writeln(exitCodeMessage); } @@ -813,7 +661,7 @@ export class TerminalInstance implements ITerminalInstance { } else { this.dispose(); if (exitCode) { - if (this._processState === ProcessState.KILLED_DURING_LAUNCH) { + if (this._processManager.processState === ProcessState.KILLED_DURING_LAUNCH) { let args = ''; if (typeof this._shellLaunchConfig.args === 'string') { args = this._shellLaunchConfig.args; @@ -825,7 +673,11 @@ export class TerminalInstance implements ITerminalInstance { return a; }).join(' '); } - this._notificationService.error(nls.localize('terminal.integrated.launchFailed', 'The terminal process command \'{0}{1}\' failed to launch (exit code: {2})', this._shellLaunchConfig.executable, args, exitCode)); + if (this._shellLaunchConfig.executable) { + this._notificationService.error(nls.localize('terminal.integrated.launchFailed', 'The terminal process command \'{0}{1}\' failed to launch (exit code: {2})', this._shellLaunchConfig.executable, args, exitCode)); + } else { + this._notificationService.error(nls.localize('terminal.integrated.launchFailedExtHost', 'The terminal process failed to launch (exit code: {0})', exitCode)); + } } else { if (this._configHelper.config.showExitAlert) { this._notificationService.error(exitCodeMessage); @@ -838,23 +690,15 @@ export class TerminalInstance implements ITerminalInstance { } private _attachPressAnyKeyToCloseListener() { - this._processDisposables.push(dom.addDisposableListener(this._xterm.textarea, 'keypress', (event: KeyboardEvent) => { + this._processManager.addDisposable(dom.addDisposableListener(this._xterm.textarea, 'keypress', (event: KeyboardEvent) => { this.dispose(); event.preventDefault(); })); } public reuseTerminal(shell?: IShellLaunchConfig): void { - // Kill and clean up old process - if (this._process) { - this._process.removeAllListeners('exit'); - if (this._process.connected) { - this._process.kill(); - } - this._process = null; - } - lifecycle.dispose(this._processDisposables); - this._processDisposables = []; + // Kill and clear up the process, making the process manager ready for a new process + this._processManager.dispose(); // Ensure new processes' output starts at start of new line this._xterm.write('\n\x1b[G'); @@ -871,7 +715,7 @@ export class TerminalInstance implements ITerminalInstance { if (oldTitle !== this._title) { this.setTitle(this._title, true); } - this._process.on('message', (message) => this._sendPtyDataToXterm(message)); + this._processManager.onProcessData(data => this._sendPtyDataToXterm(data)); // Clean up waitOnExit state if (this._isExiting && this._shellLaunchConfig.waitOnExit) { @@ -883,69 +727,6 @@ export class TerminalInstance implements ITerminalInstance { this._shellLaunchConfig = shell; } - public static mergeEnvironments(parent: IStringDictionary, other: IStringDictionary) { - if (!other) { - return; - } - - // On Windows apply the new values ignoring case, while still retaining - // the case of the original key. - if (platform.isWindows) { - for (let configKey in other) { - let actualKey = configKey; - for (let envKey in parent) { - if (configKey.toLowerCase() === envKey.toLowerCase()) { - actualKey = envKey; - break; - } - } - const value = other[configKey]; - TerminalInstance._mergeEnvironmentValue(parent, actualKey, value); - } - } else { - Object.keys(other).forEach((key) => { - const value = other[key]; - TerminalInstance._mergeEnvironmentValue(parent, key, value); - }); - } - } - - private static _mergeEnvironmentValue(env: IStringDictionary, key: string, value: string | null) { - if (typeof value === 'string') { - env[key] = value; - } else { - delete env[key]; - } - } - - // TODO: This should be private/protected - public static createTerminalEnv(parentEnv: IStringDictionary, shell: IShellLaunchConfig, cwd: string, locale: string, cols?: number, rows?: number): IStringDictionary { - const env = { ...parentEnv }; - if (shell.env) { - TerminalInstance.mergeEnvironments(env, shell.env); - } - - env['PTYPID'] = process.pid.toString(); - env['PTYSHELL'] = shell.executable; - env['TERM_PROGRAM'] = 'vscode'; - env['TERM_PROGRAM_VERSION'] = pkg.version; - if (shell.args) { - if (typeof shell.args === 'string') { - env[`PTYSHELLCMDLINE`] = shell.args; - } else { - shell.args.forEach((arg, i) => env[`PTYSHELLARG${i}`] = arg); - } - } - env['PTYCWD'] = cwd; - env['LANG'] = TerminalInstance._getLangEnvVariable(locale); - if (cols && rows) { - env['PTYCOLS'] = cols.toString(); - env['PTYROWS'] = rows.toString(); - } - env['AMD_ENTRYPOINT'] = 'vs/workbench/parts/terminal/node/terminalProcess'; - return env; - } - public onLineData(listener: (lineData: string) => void): lifecycle.IDisposable { this._onLineDataListeners.push(listener); return { @@ -984,57 +765,7 @@ export class TerminalInstance implements ITerminalInstance { } public onExit(listener: (exitCode: number) => void): lifecycle.IDisposable { - if (this._process) { - this._process.on('exit', listener); - } - return { - dispose: () => { - if (this._process) { - this._process.removeListener('exit', listener); - } - } - }; - } - - private static _sanitizeCwd(cwd: string) { - // Make the drive letter uppercase on Windows (see #9448) - if (platform.platform === platform.Platform.Windows && cwd && cwd[1] === ':') { - return cwd[0].toUpperCase() + cwd.substr(1); - } - return cwd; - } - - private static _getLangEnvVariable(locale?: string) { - const parts = locale ? locale.split('-') : []; - const n = parts.length; - if (n === 0) { - // Fallback to en_US to prevent possible encoding issues. - return 'en_US.UTF-8'; - } - if (n === 1) { - // app.getLocale can return just a language without a variant, fill in the variant for - // supported languages as many shells expect a 2-part locale. - const languageVariants = { - de: 'DE', - en: 'US', - es: 'ES', - fi: 'FI', - fr: 'FR', - it: 'IT', - ja: 'JP', - ko: 'KR', - pl: 'PL', - ru: 'RU', - zh: 'CN' - }; - if (parts[0] in languageVariants) { - parts.push(languageVariants[parts[0]]); - } - } else { - // Ensure the variant is uppercase - parts[1] = parts[1].toUpperCase(); - } - return parts.join('_') + '.UTF-8'; + return this._processManager.onProcessExit(listener); } public updateConfig(): void { @@ -1103,7 +834,7 @@ export class TerminalInstance implements ITerminalInstance { } } - public layout(dimension: Dimension): void { + public layout(dimension: dom.Dimension): void { if (this.disableLayout) { return; } @@ -1147,23 +878,9 @@ export class TerminalInstance implements ITerminalInstance { } } - this._processReady.then(() => { - if (this._process && this._process.connected) { - // The child process could aready be terminated - try { - this._process.send({ - event: 'resize', - cols: this._cols, - rows: this._rows - }); - } catch (error) { - // We tried to write to a closed pipe / channel. - if (error.code !== 'EPIPE' && error.code !== 'ERR_IPC_CHANNEL_CLOSED') { - throw (error); - } - } - } - }); + if (this._processManager) { + this._processManager.ptyProcessReady.then(() => this._processManager.setDimensions(this._cols, this._rows)); + } } public setTitle(title: string, eventFromProcess: boolean): void { @@ -1171,7 +888,7 @@ export class TerminalInstance implements ITerminalInstance { return; } if (eventFromProcess) { - title = path.basename(title); + title = paths.basename(title); if (platform.isWindows) { // Remove the .exe extension title = title.split('.exe')[0]; @@ -1179,9 +896,9 @@ export class TerminalInstance implements ITerminalInstance { } else { // If the title has not been set by the API or the rename command, unregister the handler that // automatically updates the terminal name - if (this._process && this._messageTitleListener) { - this._process.removeListener('message', this._messageTitleListener); - this._messageTitleListener = null; + if (this._messageTitleDisposable) { + lifecycle.dispose(this._messageTitleDisposable); + this._messageTitleDisposable = null; } } const didTitleChange = title !== this._title; diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminalPanel.ts b/src/vs/workbench/parts/terminal/electron-browser/terminalPanel.ts index 21926d694f0..d07fca022b7 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/terminalPanel.ts +++ b/src/vs/workbench/parts/terminal/electron-browser/terminalPanel.ts @@ -3,21 +3,19 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import * as dom from 'vs/base/browser/dom'; import * as nls from 'vs/nls'; import * as platform from 'vs/base/common/platform'; +import * as terminalEnvironment from 'vs/workbench/parts/terminal/node/terminalEnvironment'; import { Action, IAction } from 'vs/base/common/actions'; -import { Builder, Dimension } from 'vs/base/browser/builder'; import { IActionItem, Separator } from 'vs/base/browser/ui/actionbar/actionbar'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { ITerminalService, TERMINAL_PANEL_ID } from 'vs/workbench/parts/terminal/common/terminal'; -import { IThemeService, ITheme } from 'vs/platform/theme/common/themeService'; -import { TerminalFindWidget } from './terminalFindWidget'; +import { IThemeService, ITheme, registerThemingParticipant, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; +import { TerminalFindWidget } from 'vs/workbench/parts/terminal/browser/terminalFindWidget'; import { editorHoverBackground, editorHoverBorder, editorForeground } from 'vs/platform/theme/common/colorRegistry'; import { KillTerminalAction, SwitchTerminalAction, SwitchTerminalActionItem, CopyTerminalSelectionAction, TerminalPasteAction, ClearTerminalAction, SelectAllTerminalAction, CreateNewTerminalAction, SplitTerminalAction } from 'vs/workbench/parts/terminal/electron-browser/terminalActions'; import { Panel } from 'vs/workbench/browser/panel'; @@ -25,12 +23,9 @@ import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { TPromise } from 'vs/base/common/winjs.base'; import URI from 'vs/base/common/uri'; import { PANEL_BACKGROUND, PANEL_BORDER } from 'vs/workbench/common/theme'; -import { TERMINAL_BACKGROUND_COLOR, TERMINAL_BORDER_COLOR } from 'vs/workbench/parts/terminal/electron-browser/terminalColorRegistry'; +import { TERMINAL_BACKGROUND_COLOR, TERMINAL_BORDER_COLOR } from 'vs/workbench/parts/terminal/common/terminalColorRegistry'; import { DataTransfers } from 'vs/base/browser/dnd'; import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; -import { ipcRenderer as ipc } from 'electron'; -import { IOpenFileRequest } from 'vs/platform/windows/common/windows'; -import { whenDeleted } from 'vs/base/node/pfs'; export class TerminalPanel extends Panel { @@ -41,7 +36,6 @@ export class TerminalPanel extends Panel { private _fontStyleElement: HTMLElement; private _parentDomElement: HTMLElement; private _terminalContainer: HTMLElement; - private _themeStyleElement: HTMLElement; private _findWidget: TerminalFindWidget; constructor( @@ -56,11 +50,10 @@ export class TerminalPanel extends Panel { super(TERMINAL_PANEL_ID, telemetryService, themeService); } - public create(parent: Builder): TPromise { + public create(parent: HTMLElement): TPromise { super.create(parent); - this._parentDomElement = parent.getHTMLElement(); + this._parentDomElement = parent; dom.addClass(this._parentDomElement, 'integrated-terminal'); - this._themeStyleElement = document.createElement('style'); this._fontStyleElement = document.createElement('style'); this._terminalContainer = document.createElement('div'); @@ -68,14 +61,13 @@ export class TerminalPanel extends Panel { this._findWidget = this._instantiationService.createInstance(TerminalFindWidget); - this._parentDomElement.appendChild(this._themeStyleElement); this._parentDomElement.appendChild(this._fontStyleElement); this._parentDomElement.appendChild(this._terminalContainer); this._parentDomElement.appendChild(this._findWidget.getDomNode()); this._attachEventListeners(); - this._terminalService.setContainers(this.getContainer().getHTMLElement(), this._terminalContainer); + this._terminalService.setContainers(this.getContainer(), this._terminalContainer); this._register(this.themeService.onThemeChange(theme => this._updateTheme(theme))); this._register(this._configurationService.onDidChangeConfiguration(e => { @@ -86,21 +78,12 @@ export class TerminalPanel extends Panel { this._updateFont(); this._updateTheme(); - ipc.on('vscode:openFiles', (_event: any, request: IOpenFileRequest) => { - // if the request to open files is coming in from the integrated terminal (identified though - // the termProgram variable) and we are instructed to wait for editors close, wait for the - // marker file to get deleted and then focus back to the integrated terminal. - if (request.termProgram === 'vscode' && request.filesToWait) { - whenDeleted(request.filesToWait.waitMarkerFilePath).then(() => this.focus()); - } - }); - // Force another layout (first is setContainers) since config has changed - this.layout(new Dimension(this._terminalContainer.offsetWidth, this._terminalContainer.offsetHeight)); + this.layout(new dom.Dimension(this._terminalContainer.offsetWidth, this._terminalContainer.offsetHeight)); return TPromise.as(void 0); } - public layout(dimension?: Dimension): void { + public layout(dimension?: dom.Dimension): void { if (!dimension) { return; } @@ -128,7 +111,7 @@ export class TerminalPanel extends Panel { return; } - const instance = this._terminalService.createInstance(); + const instance = this._terminalService.createTerminal(); if (instance) { this._updateFont(); this._updateTheme(); @@ -306,7 +289,7 @@ export class TerminalPanel extends Panel { } const terminal = this._terminalService.getActiveInstance(); - terminal.sendText(TerminalPanel.preparePathForTerminal(path), false); + terminal.sendText(terminalEnvironment.preparePathForTerminal(path), false); } })); } @@ -316,31 +299,6 @@ export class TerminalPanel extends Panel { theme = this.themeService.getTheme(); } - let css = ''; - - const backgroundColor = theme.getColor(TERMINAL_BACKGROUND_COLOR) || theme.getColor(PANEL_BACKGROUND); - this._terminalContainer.style.backgroundColor = backgroundColor ? backgroundColor.toString() : ''; - - const borderColor = theme.getColor(TERMINAL_BORDER_COLOR) || theme.getColor(PANEL_BORDER); - if (borderColor) { - css += `.monaco-workbench .panel.integrated-terminal .split-view-view:not(:first-child) { border-color: ${borderColor.toString()}; }`; - } - - // Borrow the editor's hover background for now - let hoverBackground = theme.getColor(editorHoverBackground); - if (hoverBackground) { - css += `.monaco-workbench .panel.integrated-terminal .terminal-message-widget { background-color: ${hoverBackground}; }`; - } - let hoverBorder = theme.getColor(editorHoverBorder); - if (hoverBorder) { - css += `.monaco-workbench .panel.integrated-terminal .terminal-message-widget { border: 1px solid ${hoverBorder}; }`; - } - let hoverForeground = theme.getColor(editorForeground); - if (hoverForeground) { - css += `.monaco-workbench .panel.integrated-terminal .terminal-message-widget { color: ${hoverForeground}; }`; - } - - this._themeStyleElement.innerHTML = css; this._findWidget.updateTheme(theme); } @@ -350,30 +308,30 @@ export class TerminalPanel extends Panel { } // TODO: Can we support ligatures? // dom.toggleClass(this._parentDomElement, 'enable-ligatures', this._terminalService.configHelper.config.fontLigatures); - this.layout(new Dimension(this._parentDomElement.offsetWidth, this._parentDomElement.offsetHeight)); - } - - /** - * Adds quotes to a path if it contains whitespaces - */ - public static preparePathForTerminal(path: string): string { - if (platform.isWindows) { - if (/\s+/.test(path)) { - return `"${path}"`; - } - return path; - } - path = path.replace(/(%5C|\\)/g, '\\\\'); - const charsToEscape = [ - ' ', '\'', '"', '?', ':', ';', '!', '*', '(', ')', '{', '}', '[', ']' - ]; - for (let i = 0; i < path.length; i++) { - const indexOfChar = charsToEscape.indexOf(path.charAt(i)); - if (indexOfChar >= 0) { - path = `${path.substring(0, i)}\\${path.charAt(i)}${path.substring(i + 1)}`; - i++; // Skip char due to escape char being added - } - } - return path; + this.layout(new dom.Dimension(this._parentDomElement.offsetWidth, this._parentDomElement.offsetHeight)); } } + +registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => { + const backgroundColor = theme.getColor(TERMINAL_BACKGROUND_COLOR) || theme.getColor(PANEL_BACKGROUND); + collector.addRule(`.monaco-workbench .panel.integrated-terminal .terminal-outer-container { background-color: ${backgroundColor ? backgroundColor.toString() : ''}; }`); + + const borderColor = theme.getColor(TERMINAL_BORDER_COLOR) || theme.getColor(PANEL_BORDER); + if (borderColor) { + collector.addRule(`.monaco-workbench .panel.integrated-terminal .split-view-view:not(:first-child) { border-color: ${borderColor.toString()}; }`); + } + + // Borrow the editor's hover background for now + let hoverBackground = theme.getColor(editorHoverBackground); + if (hoverBackground) { + collector.addRule(`.monaco-workbench .panel.integrated-terminal .terminal-message-widget { background-color: ${hoverBackground}; }`); + } + let hoverBorder = theme.getColor(editorHoverBorder); + if (hoverBorder) { + collector.addRule(`.monaco-workbench .panel.integrated-terminal .terminal-message-widget { border: 1px solid ${hoverBorder}; }`); + } + let hoverForeground = theme.getColor(editorForeground); + if (hoverForeground) { + collector.addRule(`.monaco-workbench .panel.integrated-terminal .terminal-message-widget { color: ${hoverForeground}; }`); + } +}); \ No newline at end of file diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminalProcessManager.ts b/src/vs/workbench/parts/terminal/electron-browser/terminalProcessManager.ts new file mode 100644 index 00000000000..1670b248a0b --- /dev/null +++ b/src/vs/workbench/parts/terminal/electron-browser/terminalProcessManager.ts @@ -0,0 +1,207 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as cp from 'child_process'; +import * as platform from 'vs/base/common/platform'; +import * as terminalEnvironment from 'vs/workbench/parts/terminal/node/terminalEnvironment'; +import Uri from 'vs/base/common/uri'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import { ProcessState, ITerminalProcessManager, IShellLaunchConfig, ITerminalConfigHelper } from 'vs/workbench/parts/terminal/common/terminal'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { ILogService } from 'vs/platform/log/common/log'; +import { Emitter, Event } from 'vs/base/common/event'; +import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { IHistoryService } from 'vs/workbench/services/history/common/history'; +import { ITerminalChildProcess, IMessageFromTerminalProcess } from 'vs/workbench/parts/terminal/node/terminal'; +import { TerminalProcessExtHostProxy } from 'vs/workbench/parts/terminal/node/terminalProcessExtHostProxy'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; + +/** The amount of time to consider terminal errors to be related to the launch */ +const LAUNCHING_DURATION = 500; + +/** + * Holds all state related to the creation and management of terminal processes. + * + * Internal definitions: + * - Process: The process launched with the terminalProcess.ts file, or the pty as a whole + * - Pty Process: The pseudoterminal master process (or the winpty agent process) + * - Shell Process: The pseudoterminal slave process (ie. the shell) + */ +export class TerminalProcessManager implements ITerminalProcessManager { + public processState: ProcessState = ProcessState.UNINITIALIZED; + public ptyProcessReady: TPromise; + public shellProcessId: number; + public initialCwd: string; + + private _process: ITerminalChildProcess; + private _preLaunchInputQueue: string[] = []; + private _disposables: IDisposable[] = []; + + private readonly _onProcessReady: Emitter = new Emitter(); + public get onProcessReady(): Event { return this._onProcessReady.event; } + private readonly _onProcessData: Emitter = new Emitter(); + public get onProcessData(): Event { return this._onProcessData.event; } + private readonly _onProcessTitle: Emitter = new Emitter(); + public get onProcessTitle(): Event { return this._onProcessTitle.event; } + private readonly _onProcessExit: Emitter = new Emitter(); + public get onProcessExit(): Event { return this._onProcessExit.event; } + + constructor( + private _terminalId: number, + private _configHelper: ITerminalConfigHelper, + @IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService, + @IHistoryService private readonly _historyService: IHistoryService, + @IConfigurationResolverService private readonly _configurationResolverService: IConfigurationResolverService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @ILogService private _logService: ILogService + ) { + this.ptyProcessReady = new TPromise(c => { + this.onProcessReady(() => { + this._logService.debug(`Terminal process ready (shellProcessId: ${this.shellProcessId})`); + c(void 0); + }); + }); + } + + public dispose(): void { + if (this._process) { + if (this._process.connected) { + // If the process was still connected this dispose came from + // within VS Code, not the process, so mark the process as + // killed by the user. + this.processState = ProcessState.KILLED_BY_USER; + this._process.send({ event: 'shutdown' }); + } + this._process = null; + } + this._disposables.forEach(d => d.dispose()); + this._disposables.length = 0; + } + + public addDisposable(disposable: IDisposable) { + this._disposables.push(disposable); + } + + public createProcess( + shellLaunchConfig: IShellLaunchConfig, + cols: number, + rows: number + ): void { + const extensionHostOwned = (this._configHelper.config).extHostProcess; + if (extensionHostOwned) { + this._process = this._instantiationService.createInstance(TerminalProcessExtHostProxy, this._terminalId, shellLaunchConfig, cols, rows); + } else { + const locale = this._configHelper.config.setLocaleVariables ? platform.locale : undefined; + if (!shellLaunchConfig.executable) { + this._configHelper.mergeDefaultShellPathAndArgs(shellLaunchConfig); + } + + const lastActiveWorkspaceRootUri = this._historyService.getLastActiveWorkspaceRoot('file'); + this.initialCwd = terminalEnvironment.getCwd(shellLaunchConfig, lastActiveWorkspaceRootUri, this._configHelper); + + // Resolve env vars from config and shell + const lastActiveWorkspaceRoot = this._workspaceContextService.getWorkspaceFolder(lastActiveWorkspaceRootUri); + const platformKey = platform.isWindows ? 'windows' : (platform.isMacintosh ? 'osx' : 'linux'); + const envFromConfig = terminalEnvironment.resolveConfigurationVariables(this._configurationResolverService, { ...this._configHelper.config.env[platformKey] }, lastActiveWorkspaceRoot); + const envFromShell = terminalEnvironment.resolveConfigurationVariables(this._configurationResolverService, { ...shellLaunchConfig.env }, lastActiveWorkspaceRoot); + shellLaunchConfig.env = envFromShell; + + // Merge process env with the env from config + const parentEnv = { ...process.env }; + terminalEnvironment.mergeEnvironments(parentEnv, envFromConfig); + + // Continue env initialization, merging in the env from the launch + // config and adding keys that are needed to create the process + const env = terminalEnvironment.createTerminalEnv(parentEnv, shellLaunchConfig, this.initialCwd, locale, cols, rows); + const cwd = Uri.parse(require.toUrl('../node')).fsPath; + const options = { env, cwd }; + this._logService.debug(`Terminal process launching`, options); + + this._process = cp.fork(Uri.parse(require.toUrl('bootstrap')).fsPath, ['--type=terminal'], options); + } + this.processState = ProcessState.LAUNCHING; + + this._process.on('message', message => this._onMessage(message)); + this._process.on('exit', exitCode => this._onExit(exitCode)); + + setTimeout(() => { + if (this.processState === ProcessState.LAUNCHING) { + this.processState = ProcessState.RUNNING; + } + }, LAUNCHING_DURATION); + } + + public setDimensions(cols: number, rows: number): void { + if (this._process && this._process.connected) { + // The child process could aready be terminated + try { + this._process.send({ event: 'resize', cols, rows }); + } catch (error) { + // We tried to write to a closed pipe / channel. + if (error.code !== 'EPIPE' && error.code !== 'ERR_IPC_CHANNEL_CLOSED') { + throw (error); + } + } + } + } + + public write(data: string): void { + if (this.shellProcessId) { + // Send data if the pty is ready + this._process.send({ + event: 'input', + data + }); + } else { + // If the pty is not ready, queue the data received to send later + this._preLaunchInputQueue.push(data); + } + } + + private _onMessage(message: IMessageFromTerminalProcess): void { + this._logService.trace(`terminalProcessManager#_onMessage (shellProcessId: ${this.shellProcessId}`, message); + switch (message.type) { + case 'data': + this._onProcessData.fire(message.content); + break; + case 'pid': + this.shellProcessId = message.content; + this._onProcessReady.fire(); + + // Send any queued data that's waiting + if (this._preLaunchInputQueue.length > 0) { + this._process.send({ + event: 'input', + data: this._preLaunchInputQueue.join('') + }); + this._preLaunchInputQueue.length = 0; + } + break; + case 'title': + this._onProcessTitle.fire(message.content); + break; + } + } + + private _onExit(exitCode: number): void { + this._process = null; + + // If the process is marked as launching then mark the process as killed + // during launch. This typically means that there is a problem with the + // shell and args. + if (this.processState === ProcessState.LAUNCHING) { + this.processState = ProcessState.KILLED_DURING_LAUNCH; + } + + // If TerminalInstance did not know about the process exit then it was + // triggered by the process, not on VS Code's side. + if (this.processState === ProcessState.RUNNING) { + this.processState = ProcessState.KILLED_BY_PROCESS; + } + + this._onProcessExit.fire(exitCode); + } +} \ No newline at end of file diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminalService.ts b/src/vs/workbench/parts/terminal/electron-browser/terminalService.ts index 54e34c38cb4..1ee8373b643 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/terminalService.ts +++ b/src/vs/workbench/parts/terminal/electron-browser/terminalService.ts @@ -6,24 +6,28 @@ import * as nls from 'vs/nls'; import * as pfs from 'vs/base/node/pfs'; import * as platform from 'vs/base/common/platform'; -import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; import { IPartService } from 'vs/workbench/services/part/common/partService'; import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; import { IQuickOpenService, IPickOpenEntry, IPickOptions } from 'vs/platform/quickOpen/common/quickOpen'; -import { ITerminalInstance, ITerminalService, IShellLaunchConfig, ITerminalConfigHelper, NEVER_SUGGEST_SELECT_WINDOWS_SHELL_STORAGE_KEY, TERMINAL_PANEL_ID } from 'vs/workbench/parts/terminal/common/terminal'; +import { ITerminalInstance, ITerminalService, IShellLaunchConfig, ITerminalConfigHelper, NEVER_SUGGEST_SELECT_WINDOWS_SHELL_STORAGE_KEY, TERMINAL_PANEL_ID, ITerminalProcessExtHostProxy } from 'vs/workbench/parts/terminal/common/terminal'; import { TerminalService as AbstractTerminalService } from 'vs/workbench/parts/terminal/common/terminalService'; import { TerminalConfigHelper } from 'vs/workbench/parts/terminal/electron-browser/terminalConfigHelper'; import { TPromise } from 'vs/base/common/winjs.base'; import Severity from 'vs/base/common/severity'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; -import { getTerminalDefaultShellWindows } from 'vs/workbench/parts/terminal/electron-browser/terminal'; +import { getTerminalDefaultShellWindows } from 'vs/workbench/parts/terminal/node/terminal'; import { TerminalPanel } from 'vs/workbench/parts/terminal/electron-browser/terminalPanel'; -import { TerminalTab } from 'vs/workbench/parts/terminal/electron-browser/terminalTab'; +import { TerminalTab } from 'vs/workbench/parts/terminal/browser/terminalTab'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { INotificationService, PromptOption } from 'vs/platform/notification/common/notification'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { ipcRenderer as ipc } from 'electron'; +import { IOpenFileRequest } from 'vs/platform/windows/common/windows'; +import { TerminalInstance } from 'vs/workbench/parts/terminal/electron-browser/terminalInstance'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; export class TerminalService extends AbstractTerminalService implements ITerminalService { private _configHelper: TerminalConfigHelper; @@ -44,15 +48,29 @@ export class TerminalService extends AbstractTerminalService implements ITermina @IInstantiationService private readonly _instantiationService: IInstantiationService, @IQuickOpenService private readonly _quickOpenService: IQuickOpenService, @INotificationService private readonly _notificationService: INotificationService, - @IDialogService private readonly _dialogService: IDialogService + @IDialogService private readonly _dialogService: IDialogService, + @IExtensionService private readonly _extensionService: IExtensionService ) { super(contextKeyService, panelService, partService, lifecycleService, storageService); this._terminalTabs = []; this._configHelper = this._instantiationService.createInstance(TerminalConfigHelper); + + ipc.on('vscode:openFiles', (_event: any, request: IOpenFileRequest) => { + // if the request to open files is coming in from the integrated terminal (identified though + // the termProgram variable) and we are instructed to wait for editors close, wait for the + // marker file to get deleted and then focus back to the integrated terminal. + if (request.termProgram === 'vscode' && request.filesToWait) { + pfs.whenDeleted(request.filesToWait.waitMarkerFilePath).then(() => { + if (this.terminalInstances.length > 0) { + this.getActiveInstance().focus(); + } + }); + } + }); } - public createInstance(shell: IShellLaunchConfig = {}, wasNewTerminalAction?: boolean): ITerminalInstance { + public createTerminal(shell: IShellLaunchConfig = {}, wasNewTerminalAction?: boolean): ITerminalInstance { const terminalTab = this._instantiationService.createInstance(TerminalTab, this._terminalFocusContextKey, this._configHelper, @@ -67,11 +85,26 @@ export class TerminalService extends AbstractTerminalService implements ITermina // It's the first instance so it should be made active automatically this.setActiveInstanceByIndex(0); } + this._onInstanceCreated.fire(instance); this._onInstancesChanged.fire(); this._suggestShellChange(wasNewTerminalAction); return instance; } + public createInstance(terminalFocusContextKey: IContextKey, configHelper: ITerminalConfigHelper, container: HTMLElement, shellLaunchConfig: IShellLaunchConfig, doCreateProcess: boolean): ITerminalInstance { + return this._instantiationService.createInstance(TerminalInstance, terminalFocusContextKey, configHelper, container, shellLaunchConfig, true); + } + + public requestExtHostProcess(proxy: ITerminalProcessExtHostProxy, shellLaunchConfig: IShellLaunchConfig, cols: number, rows: number): void { + // Ensure extension host is ready before requesting a process + this._extensionService.whenInstalledExtensionsRegistered().then(() => { + // TODO: MainThreadTerminalService is not ready at this point, fix this + setTimeout(() => { + this._onInstanceRequestExtHostProcess.fire({ proxy, shellLaunchConfig, cols, rows }); + }, 500); + }); + } + public focusFindWidget(): TPromise { return this.showPanel(false).then(() => { let panel = this._panelService.getActivePanel() as TerminalPanel; @@ -127,17 +160,18 @@ export class TerminalService extends AbstractTerminalService implements ITermina return; } - const message = nls.localize('terminal.integrated.chooseWindowsShellInfo', "You can change the default terminal shell by selecting the customize button."); - const options: PromptOption[] = [nls.localize('customize', "Customize"), { label: nls.localize('never again', "Don't Show Again") }]; - this._notificationService.prompt(Severity.Info, message, options).then(choice => { - switch (choice) { - case 0 /* Customize */: + this._notificationService.prompt( + Severity.Info, + nls.localize('terminal.integrated.chooseWindowsShellInfo', "You can change the default terminal shell by selecting the customize button."), + [{ + label: nls.localize('customize', "Customize"), + run: () => { this.selectDefaultWindowsShell().then(shell => { if (!shell) { return TPromise.as(null); } // Launch a new instance with the newly selected shell - const instance = this.createInstance({ + const instance = this.createTerminal({ executable: shell, args: this._configHelper.config.shellArgs.windows }); @@ -146,12 +180,14 @@ export class TerminalService extends AbstractTerminalService implements ITermina } return TPromise.as(null); }); - break; - case 1 /* Do not show again */: - this._storageService.store(NEVER_SUGGEST_SELECT_WINDOWS_SHELL_STORAGE_KEY, true); - break; - } - }); + } + }, + { + label: nls.localize('never again', "Don't Show Again"), + isSecondary: true, + run: () => this._storageService.store(NEVER_SUGGEST_SELECT_WINDOWS_SHELL_STORAGE_KEY, true) + }] + ); } public selectDefaultWindowsShell(): TPromise { @@ -215,7 +251,7 @@ export class TerminalService extends AbstractTerminalService implements ITermina public getActiveOrCreateInstance(wasNewTerminalAction?: boolean): ITerminalInstance { const activeInstance = this.getActiveInstance(); - return activeInstance ? activeInstance : this.createInstance(undefined, wasNewTerminalAction); + return activeInstance ? activeInstance : this.createTerminal(undefined, wasNewTerminalAction); } protected _showTerminalCloseConfirmation(): TPromise { diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminal.ts b/src/vs/workbench/parts/terminal/node/terminal.ts similarity index 74% rename from src/vs/workbench/parts/terminal/electron-browser/terminal.ts rename to src/vs/workbench/parts/terminal/node/terminal.ts index 620dcf23b9f..8cb9bc9dddd 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/terminal.ts +++ b/src/vs/workbench/parts/terminal/node/terminal.ts @@ -2,13 +2,37 @@ * 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 os from 'os'; import * as platform from 'vs/base/common/platform'; import * as processes from 'vs/base/node/processes'; import { readFile, fileExists } from 'vs/base/node/pfs'; +export interface IMessageFromTerminalProcess { + type: 'pid' | 'data' | 'title'; + content: number | string; +} + +export interface IMessageToTerminalProcess { + event: 'resize' | 'input' | 'shutdown'; + data?: string; + cols?: number; + rows?: number; +} + +/** + * An interface representing a raw terminal child process, this is a subset of the + * child_process.ChildProcess node.js interface. + */ +export interface ITerminalChildProcess { + readonly connected: boolean; + + send(message: IMessageToTerminalProcess): boolean; + + on(event: 'exit', listener: (code: number) => void): this; + on(event: 'message', listener: (message: IMessageFromTerminalProcess) => void): this; +} + let _TERMINAL_DEFAULT_SHELL_UNIX_LIKE: string = null; export function getTerminalDefaultShellUnixLike(): string { if (!_TERMINAL_DEFAULT_SHELL_UNIX_LIKE) { @@ -51,4 +75,4 @@ if (platform.isLinux) { }); } -export let isFedora = false; +export let isFedora = false; \ No newline at end of file diff --git a/src/vs/workbench/parts/terminal/node/terminalCommandTracker.ts b/src/vs/workbench/parts/terminal/node/terminalCommandTracker.ts index f7c06906327..7229460ec01 100644 --- a/src/vs/workbench/parts/terminal/node/terminalCommandTracker.ts +++ b/src/vs/workbench/parts/terminal/node/terminalCommandTracker.ts @@ -16,6 +16,11 @@ enum Boundary { Bottom } +export enum ScrollPosition { + Top, + Middle +} + export class TerminalCommandTracker implements ITerminalCommandTracker { private _currentMarker: IMarker | Boundary = Boundary.Bottom; private _selectionStart: IMarker | Boundary | null = null; @@ -43,7 +48,7 @@ export class TerminalCommandTracker implements ITerminalCommandTracker { } } - public focusPreviousCommand(retainSelection: boolean = false): void { + public scrollToPreviousCommand(scrollPosition: ScrollPosition = ScrollPosition.Top, retainSelection: boolean = false): void { if (!retainSelection) { this._selectionStart = null; } @@ -64,10 +69,10 @@ export class TerminalCommandTracker implements ITerminalCommandTracker { } this._currentMarker = this._xterm.markers[markerIndex]; - this._xterm.scrollToLine(this._currentMarker.line); + this._scrollToMarker(this._currentMarker, scrollPosition); } - public focusNextCommand(retainSelection: boolean = false): void { + public scrollToNextCommand(scrollPosition: ScrollPosition = ScrollPosition.Top, retainSelection: boolean = false): void { if (!retainSelection) { this._selectionStart = null; } @@ -88,14 +93,22 @@ export class TerminalCommandTracker implements ITerminalCommandTracker { } this._currentMarker = this._xterm.markers[markerIndex]; - this._xterm.scrollToLine(this._currentMarker.line); + this._scrollToMarker(this._currentMarker, scrollPosition); + } + + private _scrollToMarker(marker: IMarker, position: ScrollPosition): void { + let line = marker.line; + if (position === ScrollPosition.Middle) { + line = Math.max(line - this._xterm.rows / 2, 0); + } + this._xterm.scrollToLine(line); } public selectToPreviousCommand(): void { if (this._selectionStart === null) { this._selectionStart = this._currentMarker; } - this.focusPreviousCommand(true); + this.scrollToPreviousCommand(ScrollPosition.Middle, true); this._selectLines(this._currentMarker, this._selectionStart); } @@ -103,8 +116,7 @@ export class TerminalCommandTracker implements ITerminalCommandTracker { if (this._selectionStart === null) { this._selectionStart = this._currentMarker; } - this.focusNextCommand(true); - // if (!this._currentMarker + this.scrollToNextCommand(ScrollPosition.Middle, true); this._selectLines(this._currentMarker, this._selectionStart); } diff --git a/src/vs/workbench/parts/terminal/node/terminalEnvironment.ts b/src/vs/workbench/parts/terminal/node/terminalEnvironment.ts new file mode 100644 index 00000000000..2c60754603d --- /dev/null +++ b/src/vs/workbench/parts/terminal/node/terminalEnvironment.ts @@ -0,0 +1,182 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as os from 'os'; +import * as paths from 'vs/base/common/paths'; +import * as platform from 'vs/base/common/platform'; +import pkg from 'vs/platform/node/package'; +import Uri from 'vs/base/common/uri'; +import { IStringDictionary } from 'vs/base/common/collections'; +import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; +import { IShellLaunchConfig, ITerminalConfigHelper } from 'vs/workbench/parts/terminal/common/terminal'; +import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; + +/** + * This module contains utility functions related to the environment, cwd and paths. + */ + +export function mergeEnvironments(parent: IStringDictionary, other: IStringDictionary) { + if (!other) { + return; + } + + // On Windows apply the new values ignoring case, while still retaining + // the case of the original key. + if (platform.isWindows) { + for (let configKey in other) { + let actualKey = configKey; + for (let envKey in parent) { + if (configKey.toLowerCase() === envKey.toLowerCase()) { + actualKey = envKey; + break; + } + } + const value = other[configKey]; + _mergeEnvironmentValue(parent, actualKey, value); + } + } else { + Object.keys(other).forEach((key) => { + const value = other[key]; + _mergeEnvironmentValue(parent, key, value); + }); + } +} + +function _mergeEnvironmentValue(env: IStringDictionary, key: string, value: string | null) { + if (typeof value === 'string') { + env[key] = value; + } else { + delete env[key]; + } +} + +export function createTerminalEnv(parentEnv: IStringDictionary, shell: IShellLaunchConfig, cwd: string, locale: string, cols?: number, rows?: number): IStringDictionary { + const env = { ...parentEnv }; + if (shell.env) { + mergeEnvironments(env, shell.env); + } + + env['PTYPID'] = process.pid.toString(); + env['PTYSHELL'] = shell.executable; + env['TERM_PROGRAM'] = 'vscode'; + env['TERM_PROGRAM_VERSION'] = pkg.version; + if (shell.args) { + if (typeof shell.args === 'string') { + env[`PTYSHELLCMDLINE`] = shell.args; + } else { + shell.args.forEach((arg, i) => env[`PTYSHELLARG${i}`] = arg); + } + } + env['PTYCWD'] = cwd; + env['LANG'] = _getLangEnvVariable(locale); + if (cols && rows) { + env['PTYCOLS'] = cols.toString(); + env['PTYROWS'] = rows.toString(); + } + env['AMD_ENTRYPOINT'] = 'vs/workbench/parts/terminal/node/terminalProcess'; + return env; +} + +export function resolveConfigurationVariables(configurationResolverService: IConfigurationResolverService, env: IStringDictionary, lastActiveWorkspaceRoot: IWorkspaceFolder): IStringDictionary { + Object.keys(env).forEach((key) => { + if (typeof env[key] === 'string') { + env[key] = configurationResolverService.resolve(lastActiveWorkspaceRoot, env[key]); + } + }); + return env; +} + +function _getLangEnvVariable(locale?: string) { + const parts = locale ? locale.split('-') : []; + const n = parts.length; + if (n === 0) { + // Fallback to en_US to prevent possible encoding issues. + return 'en_US.UTF-8'; + } + if (n === 1) { + // app.getLocale can return just a language without a variant, fill in the variant for + // supported languages as many shells expect a 2-part locale. + const languageVariants = { + de: 'DE', + en: 'US', + es: 'ES', + fi: 'FI', + fr: 'FR', + it: 'IT', + ja: 'JP', + ko: 'KR', + pl: 'PL', + ru: 'RU', + zh: 'CN' + }; + if (parts[0] in languageVariants) { + parts.push(languageVariants[parts[0]]); + } + } else { + // Ensure the variant is uppercase + parts[1] = parts[1].toUpperCase(); + } + return parts.join('_') + '.UTF-8'; +} + +export function getCwd(shell: IShellLaunchConfig, root: Uri, configHelper: ITerminalConfigHelper): string { + if (shell.cwd) { + return shell.cwd; + } + + let cwd: string; + + // TODO: Handle non-existent customCwd + if (!shell.ignoreConfigurationCwd) { + // Evaluate custom cwd first + const customCwd = configHelper.config.cwd; + if (customCwd) { + if (paths.isAbsolute(customCwd)) { + cwd = customCwd; + } else if (root) { + cwd = paths.normalize(paths.join(root.fsPath, customCwd)); + } + } + } + + // If there was no custom cwd or it was relative with no workspace + if (!cwd) { + cwd = root ? root.fsPath : os.homedir(); + } + + return _sanitizeCwd(cwd); +} + +function _sanitizeCwd(cwd: string): string { + // Make the drive letter uppercase on Windows (see #9448) + if (platform.platform === platform.Platform.Windows && cwd && cwd[1] === ':') { + return cwd[0].toUpperCase() + cwd.substr(1); + } + return cwd; +} + +/** + * Adds quotes to a path if it contains whitespaces + */ +export function preparePathForTerminal(path: string): string { + if (platform.isWindows) { + if (/\s+/.test(path)) { + return `"${path}"`; + } + return path; + } + path = path.replace(/(%5C|\\)/g, '\\\\'); + const charsToEscape = [ + ' ', '\'', '"', '?', ':', ';', '!', '*', '(', ')', '{', '}', '[', ']' + ]; + for (let i = 0; i < path.length; i++) { + const indexOfChar = charsToEscape.indexOf(path.charAt(i)); + if (indexOfChar >= 0) { + path = `${path.substring(0, i)}\\${path.charAt(i)}${path.substring(i + 1)}`; + i++; // Skip char due to escape char being added + } + } + return path; +} diff --git a/src/vs/workbench/parts/terminal/node/terminalProcess.ts b/src/vs/workbench/parts/terminal/node/terminalProcess.ts index 1bd5563acb0..e3a73831538 100644 --- a/src/vs/workbench/parts/terminal/node/terminalProcess.ts +++ b/src/vs/workbench/parts/terminal/node/terminalProcess.ts @@ -24,7 +24,7 @@ var cols = process.env.PTYCOLS; var rows = process.env.PTYROWS; var currentTitle = ''; -setupPlanB(process.env.PTYPID); +setupPlanB(Number(process.env.PTYPID)); cleanEnv(); interface IOptions { @@ -43,7 +43,7 @@ if (cols && rows) { options.rows = parseInt(rows, 10); } -var ptyProcess = pty.fork(shell, args, options); +var ptyProcess = pty.spawn(shell, args, options); var closeTimeout: number; var exitCode: number; @@ -91,7 +91,7 @@ process.on('message', function (message) { sendProcessId(); setupTitlePolling(); -function getArgs() { +function getArgs(): string | string[] { if (process.env['PTYSHELLCMDLINE']) { return process.env['PTYSHELLCMDLINE']; } diff --git a/src/vs/workbench/parts/terminal/node/terminalProcessExtHostProxy.ts b/src/vs/workbench/parts/terminal/node/terminalProcessExtHostProxy.ts new file mode 100644 index 00000000000..9c842fbbfa8 --- /dev/null +++ b/src/vs/workbench/parts/terminal/node/terminalProcessExtHostProxy.ts @@ -0,0 +1,78 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ITerminalChildProcess, IMessageToTerminalProcess, IMessageFromTerminalProcess } from 'vs/workbench/parts/terminal/node/terminal'; +import { EventEmitter } from 'events'; +import { ITerminalService, ITerminalProcessExtHostProxy, IShellLaunchConfig } from 'vs/workbench/parts/terminal/common/terminal'; +import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; + +export class TerminalProcessExtHostProxy extends EventEmitter implements ITerminalChildProcess, ITerminalProcessExtHostProxy { + // For ext host processes connected checks happen on the ext host + public connected: boolean = true; + + private _disposables: IDisposable[] = []; + + constructor( + public terminalId: number, + shellLaunchConfig: IShellLaunchConfig, + cols: number, + rows: number, + @ITerminalService private _terminalService: ITerminalService + ) { + super(); + + // TODO: Return TPromise indicating success? Teardown if failure? + this._terminalService.requestExtHostProcess(this, shellLaunchConfig, cols, rows); + } + + public dispose(): void { + this._disposables.forEach(d => d.dispose()); + this._disposables.length = 0; + } + + public emitData(data: string): void { + this.emit('message', { type: 'data', content: data } as IMessageFromTerminalProcess); + } + + public emitTitle(title: string): void { + this.emit('message', { type: 'title', content: title } as IMessageFromTerminalProcess); + } + + public emitPid(pid: number): void { + this.emit('message', { type: 'pid', content: pid } as IMessageFromTerminalProcess); + } + + public emitExit(exitCode: number): void { + this.emit('exit', exitCode); + this.dispose(); + } + + public send(message: IMessageToTerminalProcess): boolean { + switch (message.event) { + case 'input': this.emit('input', message.data); break; + case 'resize': this.emit('resize', message.cols, message.rows); break; + case 'shutdown': this.emit('shutdown'); break; + } + return true; + } + + public onInput(listener: (data: string) => void): void { + const outerListener = (data) => listener(data); + this.on('input', outerListener); + this._disposables.push(toDisposable(() => this.removeListener('input', outerListener))); + } + + public onResize(listener: (cols: number, rows: number) => void): void { + const outerListener = (cols, rows) => listener(cols, rows); + this.on('resize', outerListener); + this._disposables.push(toDisposable(() => this.removeListener('resize', outerListener))); + } + + public onShutdown(listener: () => void): void { + const outerListener = () => listener(); + this.on('shutdown', outerListener); + this._disposables.push(toDisposable(() => this.removeListener('shutdown', outerListener))); + } +} \ No newline at end of file diff --git a/src/vs/workbench/parts/terminal/electron-browser/windowsShellHelper.ts b/src/vs/workbench/parts/terminal/node/windowsShellHelper.ts similarity index 98% rename from src/vs/workbench/parts/terminal/electron-browser/windowsShellHelper.ts rename to src/vs/workbench/parts/terminal/node/windowsShellHelper.ts index 803ad86dd9c..ebd8347f02f 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/windowsShellHelper.ts +++ b/src/vs/workbench/parts/terminal/node/windowsShellHelper.ts @@ -115,7 +115,7 @@ export class WindowsShellHelper { return this._currentRequest; } this._currentRequest = new TPromise(resolve => { - windowsProcessTree(this._rootProcessId, (tree) => { + windowsProcessTree.getProcessTree(this._rootProcessId, (tree) => { const name = this.traverseTree(tree); this._currentRequest = null; resolve(name); diff --git a/src/vs/workbench/parts/terminal/test/electron-browser/terminalColorRegistry.test.ts b/src/vs/workbench/parts/terminal/test/electron-browser/terminalColorRegistry.test.ts index 260a3388b13..36b11b8c146 100644 --- a/src/vs/workbench/parts/terminal/test/electron-browser/terminalColorRegistry.test.ts +++ b/src/vs/workbench/parts/terminal/test/electron-browser/terminalColorRegistry.test.ts @@ -3,12 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import * as assert from 'assert'; import { Extensions as ThemeingExtensions, IColorRegistry, ColorIdentifier } from 'vs/platform/theme/common/colorRegistry'; import { Registry } from 'vs/platform/registry/common/platform'; -import { ansiColorIdentifiers, registerColors } from 'vs/workbench/parts/terminal/electron-browser/terminalColorRegistry'; +import { ansiColorIdentifiers, registerColors } from 'vs/workbench/parts/terminal/common/terminalColorRegistry'; import { ITheme, ThemeType } from 'vs/platform/theme/common/themeService'; import { Color } from 'vs/base/common/color'; diff --git a/src/vs/workbench/parts/terminal/test/electron-browser/terminalConfigHelper.test.ts b/src/vs/workbench/parts/terminal/test/electron-browser/terminalConfigHelper.test.ts index ebd9dbfcdaa..cc4cec690c4 100644 --- a/src/vs/workbench/parts/terminal/test/electron-browser/terminalConfigHelper.test.ts +++ b/src/vs/workbench/parts/terminal/test/electron-browser/terminalConfigHelper.test.ts @@ -3,12 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import * as assert from 'assert'; import { TerminalConfigHelper } from 'vs/workbench/parts/terminal/electron-browser/terminalConfigHelper'; import { EDITOR_FONT_DEFAULTS } from 'vs/editor/common/config/editorOptions'; -import { isFedora } from 'vs/workbench/parts/terminal/electron-browser/terminal'; +import { isFedora } from 'vs/workbench/parts/terminal/node/terminal'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; suite('Workbench - TerminalConfigHelper', () => { diff --git a/src/vs/workbench/parts/terminal/test/electron-browser/terminalInstance.test.ts b/src/vs/workbench/parts/terminal/test/electron-browser/terminalInstance.test.ts deleted file mode 100644 index 5c569a30dfe..00000000000 --- a/src/vs/workbench/parts/terminal/test/electron-browser/terminalInstance.test.ts +++ /dev/null @@ -1,215 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; - -import * as assert from 'assert'; -import * as os from 'os'; -import * as platform from 'vs/base/common/platform'; -import Uri from 'vs/base/common/uri'; -import { IStringDictionary } from 'vs/base/common/collections'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { TerminalInstance } from 'vs/workbench/parts/terminal/electron-browser/terminalInstance'; -import { IShellLaunchConfig } from 'vs/workbench/parts/terminal/common/terminal'; -import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; -import { TestNotificationService, TestContextService, TestHistoryService } from 'vs/workbench/test/workbenchTestServices'; -import { MockContextKeyService, MockKeybindingService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { IHistoryService } from 'vs/workbench/services/history/common/history'; -import { TPromise } from 'vs/base/common/winjs.base'; -import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { INotificationService } from 'vs/platform/notification/common/notification'; -import { ILogService, NullLogService } from 'vs/platform/log/common/log'; - -class TestTerminalInstance extends TerminalInstance { - - public _getCwd(shell: IShellLaunchConfig, root: Uri): string { - return super._getCwd(shell, root); - } - - protected _createProcess(): void { } - protected _createXterm(): TPromise { return TPromise.as(void 0); } -} - -suite('Workbench - TerminalInstance', () => { - - let instantiationService: TestInstantiationService; - - setup(() => { - instantiationService = new TestInstantiationService(); - instantiationService.stub(INotificationService, new TestNotificationService()); - instantiationService.stub(IHistoryService, new TestHistoryService()); - }); - - test('createTerminalEnv', function () { - const shell1 = { - executable: '/bin/foosh', - args: ['-bar', 'baz'] - }; - const parentEnv1: IStringDictionary = { - ok: true - } as any; - const env1 = TerminalInstance.createTerminalEnv(parentEnv1, shell1, '/foo', 'en-au'); - assert.ok(env1['ok'], 'Parent environment is copied'); - assert.deepStrictEqual(parentEnv1, { ok: true }, 'Parent environment is unchanged'); - assert.equal(env1['PTYPID'], process.pid.toString(), 'PTYPID is equal to the current PID'); - assert.equal(env1['PTYSHELL'], '/bin/foosh', 'PTYSHELL is equal to the provided shell'); - assert.equal(env1['PTYSHELLARG0'], '-bar', 'PTYSHELLARG0 is equal to the first shell argument'); - assert.equal(env1['PTYSHELLARG1'], 'baz', 'PTYSHELLARG1 is equal to the first shell argument'); - assert.ok(!('PTYSHELLARG2' in env1), 'PTYSHELLARG2 is unset'); - assert.equal(env1['PTYCWD'], '/foo', 'PTYCWD is equal to requested cwd'); - assert.equal(env1['LANG'], 'en_AU.UTF-8', 'LANG is equal to the requested locale with UTF-8'); - - const shell2: IShellLaunchConfig = { - executable: '/bin/foosh', - args: [] - }; - const parentEnv2: IStringDictionary = { - LANG: 'en_US.UTF-8' - }; - const env2 = TerminalInstance.createTerminalEnv(parentEnv2, shell2, '/foo', 'en-au'); - assert.ok(!('PTYSHELLARG0' in env2), 'PTYSHELLARG0 is unset'); - assert.equal(env2['PTYCWD'], '/foo', 'PTYCWD is equal to /foo'); - assert.equal(env2['LANG'], 'en_AU.UTF-8', 'LANG is equal to the requested locale with UTF-8'); - - const env3 = TerminalInstance.createTerminalEnv(parentEnv1, shell1, '/', null); - assert.equal(env3['LANG'], 'en_US.UTF-8', 'LANG is equal to en_US.UTF-8 as fallback.'); // More info on issue #14586 - - const env4 = TerminalInstance.createTerminalEnv(parentEnv2, shell1, '/', null); - assert.equal(env4['LANG'], 'en_US.UTF-8', 'LANG is equal to the parent environment\'s LANG'); - }); - - suite('mergeEnvironments', () => { - test('should add keys', () => { - const parent = { - a: 'b' - }; - const other = { - c: 'd' - }; - TerminalInstance.mergeEnvironments(parent, other); - assert.deepEqual(parent, { - a: 'b', - c: 'd' - }); - }); - - test('should add keys ignoring case on Windows', () => { - if (!platform.isWindows) { - return; - } - const parent = { - a: 'b' - }; - const other = { - A: 'c' - }; - TerminalInstance.mergeEnvironments(parent, other); - assert.deepEqual(parent, { - a: 'c' - }); - }); - - test('null values should delete keys from the parent env', () => { - const parent = { - a: 'b', - c: 'd' - }; - const other: IStringDictionary = { - a: null - }; - TerminalInstance.mergeEnvironments(parent, other); - assert.deepEqual(parent, { - c: 'd' - }); - }); - - test('null values should delete keys from the parent env ignoring case on Windows', () => { - if (!platform.isWindows) { - return; - } - const parent = { - a: 'b', - c: 'd' - }; - const other: IStringDictionary = { - A: null - }; - TerminalInstance.mergeEnvironments(parent, other); - assert.deepEqual(parent, { - c: 'd' - }); - }); - }); - - suite('_getCwd', () => { - let instance: TestTerminalInstance; - let instantiationService: TestInstantiationService; - let configHelper: { config: { cwd: string } }; - - setup(() => { - let contextKeyService = new MockContextKeyService(); - let keybindingService = new MockKeybindingService(); - let terminalFocusContextKey = contextKeyService.createKey('test', false); - instantiationService = new TestInstantiationService(); - instantiationService.stub(IConfigurationService, new TestConfigurationService()); - instantiationService.stub(INotificationService, new TestNotificationService()); - instantiationService.stub(IWorkspaceContextService, new TestContextService()); - instantiationService.stub(IKeybindingService, keybindingService); - instantiationService.stub(IContextKeyService, contextKeyService); - instantiationService.stub(IHistoryService, new TestHistoryService()); - instantiationService.stub(ILogService, new NullLogService()); - configHelper = { - config: { - cwd: null - } - }; - instance = instantiationService.createInstance(TestTerminalInstance, terminalFocusContextKey, configHelper, null, null); - }); - - // This helper checks the paths in a cross-platform friendly manner - function assertPathsMatch(a: string, b: string): void { - assert.equal(Uri.file(a).fsPath, Uri.file(b).fsPath); - } - - test('should default to os.homedir() for an empty workspace', () => { - assertPathsMatch(instance._getCwd({ executable: null, args: [] }, null), os.homedir()); - }); - - test('should use to the workspace if it exists', () => { - assertPathsMatch(instance._getCwd({ executable: null, args: [] }, Uri.file('/foo')), '/foo'); - }); - - test('should use an absolute custom cwd as is', () => { - configHelper.config.cwd = '/foo'; - assertPathsMatch(instance._getCwd({ executable: null, args: [] }, null), '/foo'); - }); - - test('should normalize a relative custom cwd against the workspace path', () => { - configHelper.config.cwd = 'foo'; - assertPathsMatch(instance._getCwd({ executable: null, args: [] }, Uri.file('/bar')), '/bar/foo'); - configHelper.config.cwd = './foo'; - assertPathsMatch(instance._getCwd({ executable: null, args: [] }, Uri.file('/bar')), '/bar/foo'); - configHelper.config.cwd = '../foo'; - assertPathsMatch(instance._getCwd({ executable: null, args: [] }, Uri.file('/bar'), ), '/foo'); - }); - - test('should fall back for relative a custom cwd that doesn\'t have a workspace', () => { - configHelper.config.cwd = 'foo'; - assertPathsMatch(instance._getCwd({ executable: null, args: [] }, null), os.homedir()); - configHelper.config.cwd = './foo'; - assertPathsMatch(instance._getCwd({ executable: null, args: [] }, null), os.homedir()); - configHelper.config.cwd = '../foo'; - assertPathsMatch(instance._getCwd({ executable: null, args: [] }, null), os.homedir()); - }); - - test('should ignore custom cwd when told to ignore', () => { - configHelper.config.cwd = '/foo'; - assertPathsMatch(instance._getCwd({ executable: null, args: [], ignoreConfigurationCwd: true }, Uri.file('/bar')), '/bar'); - }); - }); -}); \ No newline at end of file diff --git a/src/vs/workbench/parts/terminal/test/electron-browser/terminalLinkHandler.test.ts b/src/vs/workbench/parts/terminal/test/electron-browser/terminalLinkHandler.test.ts index 86f24dae50d..55e629b6feb 100644 --- a/src/vs/workbench/parts/terminal/test/electron-browser/terminalLinkHandler.test.ts +++ b/src/vs/workbench/parts/terminal/test/electron-browser/terminalLinkHandler.test.ts @@ -3,8 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import * as assert from 'assert'; import { Platform } from 'vs/base/common/platform'; import { TerminalLinkHandler, LineColumnInfo } from 'vs/workbench/parts/terminal/electron-browser/terminalLinkHandler'; diff --git a/src/vs/workbench/parts/terminal/test/electron-browser/terminalPanel.test.ts b/src/vs/workbench/parts/terminal/test/electron-browser/terminalPanel.test.ts deleted file mode 100644 index 20ca2648f1b..00000000000 --- a/src/vs/workbench/parts/terminal/test/electron-browser/terminalPanel.test.ts +++ /dev/null @@ -1,22 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; - -import * as assert from 'assert'; -import { TerminalPanel } from 'vs/workbench/parts/terminal/electron-browser/terminalPanel'; -import * as platform from 'vs/base/common/platform'; - -suite('Workbench - TerminalPanel', () => { - test('preparePathForTerminal', function () { - if (platform.isWindows) { - assert.equal(TerminalPanel.preparePathForTerminal('C:\\foo'), 'C:\\foo'); - assert.equal(TerminalPanel.preparePathForTerminal('C:\\foo bar'), '"C:\\foo bar"'); - return; - } - assert.equal(TerminalPanel.preparePathForTerminal('/a/\\foo bar"\'? ;\'?? :'), '/a/\\\\foo\\ bar\\"\\\'\\?\\ \\;\\\'\\?\\?\\ \\ \\:'); - assert.equal(TerminalPanel.preparePathForTerminal('/\\\'"?:;!*(){}[]'), '/\\\\\\\'\\"\\?\\:\\;\\!\\*\\(\\)\\{\\}\\[\\]'); - }); -}); \ No newline at end of file diff --git a/src/vs/workbench/parts/terminal/test/node/terminalCommandTracker.test.ts b/src/vs/workbench/parts/terminal/test/node/terminalCommandTracker.test.ts new file mode 100644 index 00000000000..e56e7f4f6aa --- /dev/null +++ b/src/vs/workbench/parts/terminal/test/node/terminalCommandTracker.test.ts @@ -0,0 +1,114 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { Terminal } from 'vscode-xterm'; +import { TerminalCommandTracker } from 'vs/workbench/parts/terminal/node/terminalCommandTracker'; + +interface TestTerminal extends Terminal { + writeBuffer: string[]; + _innerWrite(): void; +} + +function syncWrite(term: TestTerminal, data: string): void { + // Terminal.write is asynchronous + term.writeBuffer.push(data); + term._innerWrite(); +} + +const ROWS = 10; +const COLS = 10; + +suite('Workbench - TerminalCommandTracker', () => { + let xterm: TestTerminal; + let commandTracker: TerminalCommandTracker; + + setup(() => { + xterm = (new Terminal({ + cols: COLS, + rows: ROWS + })); + // Fill initial viewport + for (let i = 0; i < ROWS - 1; i++) { + syncWrite(xterm, `${i}\n`); + } + commandTracker = new TerminalCommandTracker(xterm); + }); + + suite('Command tracking', () => { + test('should track commands when the prompt is of sufficient size', () => { + assert.equal(xterm.markers.length, 0); + syncWrite(xterm, '\x1b[3G'); // Move cursor to column 3 + xterm.emit('key', '\x0d'); + assert.equal(xterm.markers.length, 1); + }); + test('should not track commands when the prompt is too small', () => { + assert.equal(xterm.markers.length, 0); + syncWrite(xterm, '\x1b[2G'); // Move cursor to column 2 + xterm.emit('key', '\x0d'); + assert.equal(xterm.markers.length, 0); + }); + }); + + suite('Commands', () => { + test('should scroll to the next and previous commands', () => { + syncWrite(xterm, '\x1b[3G'); // Move cursor to column 3 + xterm.emit('key', '\x0d'); // Mark line #10 + assert.equal(xterm.markers[0].line, 9); + + for (let i = 0; i < 20; i++) { + syncWrite(xterm, `\r\n`); + } + assert.equal(xterm.buffer.ybase, 20); + assert.equal(xterm.buffer.ydisp, 20); + + // Scroll to marker + commandTracker.scrollToPreviousCommand(); + assert.equal(xterm.buffer.ydisp, 9); + + // Scroll to top boundary + commandTracker.scrollToPreviousCommand(); + assert.equal(xterm.buffer.ydisp, 0); + + // Scroll to marker + commandTracker.scrollToNextCommand(); + assert.equal(xterm.buffer.ydisp, 9); + + // Scroll to bottom boundary + commandTracker.scrollToNextCommand(); + assert.equal(xterm.buffer.ydisp, 20); + }); + // test('should select to the next and previous commands', () => { + // (window).matchMedia = () => { + // return { addListener: () => {} } + // }; + // xterm.open(document.createElement('div')); + + // syncWrite(xterm, '\r0'); + // syncWrite(xterm, '\n\r1'); + // syncWrite(xterm, '\x1b[3G'); // Move cursor to column 3 + // xterm.emit('key', '\x0d'); // Mark line + // assert.equal(xterm.markers[0].line, 10); + // syncWrite(xterm, '\n\r2'); + // syncWrite(xterm, '\x1b[3G'); // Move cursor to column 3 + // xterm.emit('key', '\x0d'); // Mark line + // assert.equal(xterm.markers[1].line, 11); + // syncWrite(xterm, '\n\r3'); + + // assert.equal(xterm.buffer.ybase, 3); + // assert.equal(xterm.buffer.ydisp, 3); + + // assert.equal(xterm.getSelection(), ''); + // commandTracker.selectToPreviousCommand(); + // assert.equal(xterm.getSelection(), '2'); + // commandTracker.selectToPreviousCommand(); + // assert.equal(xterm.getSelection(), '1\n2'); + // commandTracker.selectToNextCommand(); + // assert.equal(xterm.getSelection(), '2'); + // commandTracker.selectToNextCommand(); + // assert.equal(xterm.getSelection(), '\n'); + // }); + }); +}); \ No newline at end of file diff --git a/src/vs/workbench/parts/terminal/test/node/terminalEnvironment.test.ts b/src/vs/workbench/parts/terminal/test/node/terminalEnvironment.test.ts new file mode 100644 index 00000000000..5fae93b3d01 --- /dev/null +++ b/src/vs/workbench/parts/terminal/test/node/terminalEnvironment.test.ts @@ -0,0 +1,178 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import * as os from 'os'; +import * as platform from 'vs/base/common/platform'; +import * as terminalEnvironment from 'vs/workbench/parts/terminal/node/terminalEnvironment'; +import Uri from 'vs/base/common/uri'; +import { IStringDictionary } from 'vs/base/common/collections'; +import { IShellLaunchConfig, ITerminalConfigHelper } from 'vs/workbench/parts/terminal/common/terminal'; + +suite('Workbench - TerminalEnvironment', () => { + test('createTerminalEnv', function () { + const shell1 = { + executable: '/bin/foosh', + args: ['-bar', 'baz'] + }; + const parentEnv1: IStringDictionary = { + ok: true + } as any; + const env1 = terminalEnvironment.createTerminalEnv(parentEnv1, shell1, '/foo', 'en-au'); + assert.ok(env1['ok'], 'Parent environment is copied'); + assert.deepStrictEqual(parentEnv1, { ok: true }, 'Parent environment is unchanged'); + assert.equal(env1['PTYPID'], process.pid.toString(), 'PTYPID is equal to the current PID'); + assert.equal(env1['PTYSHELL'], '/bin/foosh', 'PTYSHELL is equal to the provided shell'); + assert.equal(env1['PTYSHELLARG0'], '-bar', 'PTYSHELLARG0 is equal to the first shell argument'); + assert.equal(env1['PTYSHELLARG1'], 'baz', 'PTYSHELLARG1 is equal to the first shell argument'); + assert.ok(!('PTYSHELLARG2' in env1), 'PTYSHELLARG2 is unset'); + assert.equal(env1['PTYCWD'], '/foo', 'PTYCWD is equal to requested cwd'); + assert.equal(env1['LANG'], 'en_AU.UTF-8', 'LANG is equal to the requested locale with UTF-8'); + + const shell2: IShellLaunchConfig = { + executable: '/bin/foosh', + args: [] + }; + const parentEnv2: IStringDictionary = { + LANG: 'en_US.UTF-8' + }; + const env2 = terminalEnvironment.createTerminalEnv(parentEnv2, shell2, '/foo', 'en-au'); + assert.ok(!('PTYSHELLARG0' in env2), 'PTYSHELLARG0 is unset'); + assert.equal(env2['PTYCWD'], '/foo', 'PTYCWD is equal to /foo'); + assert.equal(env2['LANG'], 'en_AU.UTF-8', 'LANG is equal to the requested locale with UTF-8'); + + const env3 = terminalEnvironment.createTerminalEnv(parentEnv1, shell1, '/', null); + assert.equal(env3['LANG'], 'en_US.UTF-8', 'LANG is equal to en_US.UTF-8 as fallback.'); // More info on issue #14586 + + const env4 = terminalEnvironment.createTerminalEnv(parentEnv2, shell1, '/', null); + assert.equal(env4['LANG'], 'en_US.UTF-8', 'LANG is equal to the parent environment\'s LANG'); + }); + + suite('mergeEnvironments', () => { + test('should add keys', () => { + const parent = { + a: 'b' + }; + const other = { + c: 'd' + }; + terminalEnvironment.mergeEnvironments(parent, other); + assert.deepEqual(parent, { + a: 'b', + c: 'd' + }); + }); + + test('should add keys ignoring case on Windows', () => { + if (!platform.isWindows) { + return; + } + const parent = { + a: 'b' + }; + const other = { + A: 'c' + }; + terminalEnvironment.mergeEnvironments(parent, other); + assert.deepEqual(parent, { + a: 'c' + }); + }); + + test('null values should delete keys from the parent env', () => { + const parent = { + a: 'b', + c: 'd' + }; + const other: IStringDictionary = { + a: null + }; + terminalEnvironment.mergeEnvironments(parent, other); + assert.deepEqual(parent, { + c: 'd' + }); + }); + + test('null values should delete keys from the parent env ignoring case on Windows', () => { + if (!platform.isWindows) { + return; + } + const parent = { + a: 'b', + c: 'd' + }; + const other: IStringDictionary = { + A: null + }; + terminalEnvironment.mergeEnvironments(parent, other); + assert.deepEqual(parent, { + c: 'd' + }); + }); + }); + + suite('getCwd', () => { + let configHelper: ITerminalConfigHelper; + + setup(() => { + configHelper = { + config: { + cwd: null + } + }; + }); + + // This helper checks the paths in a cross-platform friendly manner + function assertPathsMatch(a: string, b: string): void { + assert.equal(Uri.file(a).fsPath, Uri.file(b).fsPath); + } + + test('should default to os.homedir() for an empty workspace', () => { + assertPathsMatch(terminalEnvironment.getCwd({ executable: null, args: [] }, null, configHelper), os.homedir()); + }); + + test('should use to the workspace if it exists', () => { + assertPathsMatch(terminalEnvironment.getCwd({ executable: null, args: [] }, Uri.file('/foo'), configHelper), '/foo'); + }); + + test('should use an absolute custom cwd as is', () => { + configHelper.config.cwd = '/foo'; + assertPathsMatch(terminalEnvironment.getCwd({ executable: null, args: [] }, null, configHelper), '/foo'); + }); + + test('should normalize a relative custom cwd against the workspace path', () => { + configHelper.config.cwd = 'foo'; + assertPathsMatch(terminalEnvironment.getCwd({ executable: null, args: [] }, Uri.file('/bar'), configHelper), '/bar/foo'); + configHelper.config.cwd = './foo'; + assertPathsMatch(terminalEnvironment.getCwd({ executable: null, args: [] }, Uri.file('/bar'), configHelper), '/bar/foo'); + configHelper.config.cwd = '../foo'; + assertPathsMatch(terminalEnvironment.getCwd({ executable: null, args: [] }, Uri.file('/bar'), configHelper), '/foo'); + }); + + test('should fall back for relative a custom cwd that doesn\'t have a workspace', () => { + configHelper.config.cwd = 'foo'; + assertPathsMatch(terminalEnvironment.getCwd({ executable: null, args: [] }, null, configHelper), os.homedir()); + configHelper.config.cwd = './foo'; + assertPathsMatch(terminalEnvironment.getCwd({ executable: null, args: [] }, null, configHelper), os.homedir()); + configHelper.config.cwd = '../foo'; + assertPathsMatch(terminalEnvironment.getCwd({ executable: null, args: [] }, null, configHelper), os.homedir()); + }); + + test('should ignore custom cwd when told to ignore', () => { + configHelper.config.cwd = '/foo'; + assertPathsMatch(terminalEnvironment.getCwd({ executable: null, args: [], ignoreConfigurationCwd: true }, Uri.file('/bar'), configHelper), '/bar'); + }); + }); + + test('preparePathForTerminal', function () { + if (platform.isWindows) { + assert.equal(terminalEnvironment.preparePathForTerminal('C:\\foo'), 'C:\\foo'); + assert.equal(terminalEnvironment.preparePathForTerminal('C:\\foo bar'), '"C:\\foo bar"'); + return; + } + assert.equal(terminalEnvironment.preparePathForTerminal('/a/\\foo bar"\'? ;\'?? :'), '/a/\\\\foo\\ bar\\"\\\'\\?\\ \\;\\\'\\?\\?\\ \\ \\:'); + assert.equal(terminalEnvironment.preparePathForTerminal('/\\\'"?:;!*(){}[]'), '/\\\\\\\'\\"\\?\\:\\;\\!\\*\\(\\)\\{\\}\\[\\]'); + }); +}); \ No newline at end of file diff --git a/src/vs/workbench/parts/trust/electron-browser/unsupportedWorkspaceSettings.contribution.ts b/src/vs/workbench/parts/trust/electron-browser/unsupportedWorkspaceSettings.contribution.ts deleted file mode 100644 index 7d2c890eafc..00000000000 --- a/src/vs/workbench/parts/trust/electron-browser/unsupportedWorkspaceSettings.contribution.ts +++ /dev/null @@ -1,79 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; - -import * as nls from 'vs/nls'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { IWorkbenchContributionsRegistry, IWorkbenchContribution, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; -import { IPreferencesService } from 'vs/workbench/parts/preferences/common/preferences'; -import { IWorkspaceConfigurationService } from 'vs/workbench/services/configuration/common/configuration'; -import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { Severity, INotificationService, PromptOption } from 'vs/platform/notification/common/notification'; - -class UnsupportedWorkspaceSettingsContribution implements IWorkbenchContribution { - - private static readonly storageKey = 'workspace.settings.unsupported.warning'; - private toDispose: IDisposable[] = []; - private isUntrusted = false; - - constructor( - @ILifecycleService lifecycleService: ILifecycleService, - @IWorkspaceContextService workspaceContextService: IWorkspaceContextService, - @IWorkspaceConfigurationService private workspaceConfigurationService: IWorkspaceConfigurationService, - @IPreferencesService private preferencesService: IPreferencesService, - @IStorageService private storageService: IStorageService, - @INotificationService private notificationService: INotificationService - ) { - lifecycleService.onShutdown(this.dispose, this); - this.toDispose.push(this.workspaceConfigurationService.onDidChangeConfiguration(e => this.checkWorkspaceSettings())); - this.toDispose.push(workspaceContextService.onDidChangeWorkspaceFolders(e => this.checkWorkspaceSettings())); - } - - public dispose(): void { - this.toDispose = dispose(this.toDispose); - } - - private checkWorkspaceSettings(): void { - if (this.isUntrusted) { - return; - } - - const configurationKeys = this.workspaceConfigurationService.getUnsupportedWorkspaceKeys(); - this.isUntrusted = configurationKeys.length > 0; - if (this.isUntrusted && !this.hasShownWarning()) { - this.showWarning(configurationKeys); - } - } - - private hasShownWarning(): boolean { - return this.storageService.getBoolean(UnsupportedWorkspaceSettingsContribution.storageKey, StorageScope.WORKSPACE, false); - } - - private rememberWarningWasShown(): void { - this.storageService.store(UnsupportedWorkspaceSettingsContribution.storageKey, true, StorageScope.WORKSPACE); - } - - private showWarning(unsupportedKeys: string[]): void { - const choices: PromptOption[] = [nls.localize('openWorkspaceSettings', 'Open Workspace Settings'), { label: nls.localize('dontShowAgain', 'Don\'t Show Again') }]; - this.notificationService.prompt(Severity.Warning, nls.localize('unsupportedWorkspaceSettings', 'This Workspace contains settings that can only be set in User Settings ({0}). Click [here]({1}) to learn more.', unsupportedKeys.join(', '), 'https://go.microsoft.com/fwlink/?linkid=839878'), choices).then(choice => { - switch (choice) { - case 0 /* Open Workspace Settings */: - this.rememberWarningWasShown(); - this.preferencesService.openWorkspaceSettings(); - break; - case 1 /* Never show again */: - this.rememberWarningWasShown(); - break; - } - }); - } -} - -const workbenchRegistry = Registry.as(WorkbenchExtensions.Workbench); -workbenchRegistry.registerWorkbenchContribution(UnsupportedWorkspaceSettingsContribution, LifecyclePhase.Running); diff --git a/src/vs/workbench/parts/update/electron-browser/releaseNotesEditor.ts b/src/vs/workbench/parts/update/electron-browser/releaseNotesEditor.ts index bf6f7c9feca..30e519743fc 100644 --- a/src/vs/workbench/parts/update/electron-browser/releaseNotesEditor.ts +++ b/src/vs/workbench/parts/update/electron-browser/releaseNotesEditor.ts @@ -5,29 +5,29 @@ 'use strict'; -import { TPromise } from 'vs/base/common/winjs.base'; +import { onUnexpectedError } from 'vs/base/common/errors'; import { marked } from 'vs/base/common/marked/marked'; -import { IModeService } from 'vs/editor/common/services/modeService'; -import { tokenizeToString } from 'vs/editor/common/modes/textToHtmlTokenizer'; +import { OS } from 'vs/base/common/platform'; +import URI from 'vs/base/common/uri'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { asText } from 'vs/base/node/request'; import { IMode, TokenizationRegistry } from 'vs/editor/common/modes'; import { generateTokensCSSForColorMap } from 'vs/editor/common/modes/supports/tokenization'; -import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { KeybindingIO } from 'vs/workbench/services/keybinding/common/keybindingIO'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { IRequestService } from 'vs/platform/request/node/request'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { IPartService } from 'vs/workbench/services/part/common/partService'; -import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { WebviewInput } from 'vs/workbench/parts/webview/electron-browser/webviewInput'; -import { onUnexpectedError } from 'vs/base/common/errors'; -import { addGAParameters } from 'vs/platform/telemetry/node/telemetryNodeUtils'; -import URI from 'vs/base/common/uri'; -import { asText } from 'vs/base/node/request'; +import { tokenizeToString } from 'vs/editor/common/modes/textToHtmlTokenizer'; +import { IModeService } from 'vs/editor/common/services/modeService'; import * as nls from 'vs/nls'; -import { OS } from 'vs/base/common/platform'; -import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { IRequestService } from 'vs/platform/request/node/request'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { addGAParameters } from 'vs/platform/telemetry/node/telemetryNodeUtils'; +import { IWebviewEditorService } from 'vs/workbench/parts/webview/electron-browser/webviewEditorService'; +import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { KeybindingIO } from 'vs/workbench/services/keybinding/common/keybindingIO'; +import { Position } from 'vs/platform/editor/common/editor'; +import { WebviewEditorInput } from 'vs/workbench/parts/webview/electron-browser/webviewEditorInput'; function renderBody( body: string, @@ -51,18 +51,17 @@ export class ReleaseNotesManager { private _releaseNotesCache: { [version: string]: TPromise; } = Object.create(null); - private _currentReleaseNotes: WebviewInput | undefined = undefined; + private _currentReleaseNotes: WebviewEditorInput | undefined = undefined; public constructor( - @IEditorGroupService private readonly _editorGroupService: IEditorGroupService, @IEnvironmentService private readonly _environmentService: IEnvironmentService, @IKeybindingService private readonly _keybindingService: IKeybindingService, @IModeService private readonly _modeService: IModeService, @IOpenerService private readonly _openerService: IOpenerService, - @IPartService private readonly _partService: IPartService, @IRequestService private readonly _requestService: IRequestService, @ITelemetryService private readonly _telemetryService: ITelemetryService, @IWorkbenchEditorService private readonly _editorService: IWorkbenchEditorService, + @IWebviewEditorService private readonly _webviewEditorService: IWebviewEditorService, ) { } public async show( @@ -73,21 +72,23 @@ export class ReleaseNotesManager { const html = await this.renderBody(releaseNoteText); const title = nls.localize('releaseNotesInputName', "Release Notes: {0}", version); + const activeEditor = this._editorService.getActiveEditor(); if (this._currentReleaseNotes) { this._currentReleaseNotes.setName(title); - this._currentReleaseNotes.setHtml(html); - const activeEditor = this._editorService.getActiveEditor(); - if (activeEditor && activeEditor.position !== this._currentReleaseNotes.position) { - this._editorGroupService.moveEditor(this._currentReleaseNotes, this._currentReleaseNotes.position, activeEditor.position, { preserveFocus: true }); - } else { - this._editorService.openEditor(this._currentReleaseNotes, { preserveFocus: true }); - } + this._currentReleaseNotes.html = html; + this._webviewEditorService.revealWebview(this._currentReleaseNotes, activeEditor ? activeEditor.position : undefined); } else { - this._currentReleaseNotes = new WebviewInput(title, { tryRestoreScrollPosition: true, enableFindWidget: true }, html, { - onDidClickLink: uri => this.onDidClickLink(uri), - onDispose: () => { this._currentReleaseNotes = undefined; } - }, this._partService); - await this._editorService.openEditor(this._currentReleaseNotes, { pinned: true }); + this._currentReleaseNotes = this._webviewEditorService.createWebview( + 'releaseNotes', + title, + activeEditor ? activeEditor.position : Position.ONE, + { tryRestoreScrollPosition: true, enableFindWidget: true }, + undefined, { + onDidClickLink: uri => this.onDidClickLink(uri), + onDispose: () => { this._currentReleaseNotes = undefined; } + }); + + this._currentReleaseNotes.html = html; } return true; diff --git a/src/vs/workbench/parts/update/electron-browser/update.ts b/src/vs/workbench/parts/update/electron-browser/update.ts index e72d54cc255..07ce8b15539 100644 --- a/src/vs/workbench/parts/update/electron-browser/update.ts +++ b/src/vs/workbench/parts/update/electron-browser/update.ts @@ -29,15 +29,7 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IWindowService } from 'vs/platform/windows/common/windows'; import { ReleaseNotesManager } from './releaseNotesEditor'; -import { once } from 'vs/base/common/event'; - -const NotNowAction = new Action( - 'update.later', - nls.localize('later', "Later"), - null, - true, - () => TPromise.as(true) -); +import { isWindows } from 'vs/base/common/platform'; let releaseNotesManager: ReleaseNotesManager | undefined = undefined; @@ -122,30 +114,32 @@ export class ProductContribution implements IWorkbenchContribution { @IInstantiationService instantiationService: IInstantiationService, @INotificationService notificationService: INotificationService, @IWorkbenchEditorService editorService: IWorkbenchEditorService, - @IEnvironmentService environmentService: IEnvironmentService + @IEnvironmentService environmentService: IEnvironmentService, + @IOpenerService openerService: IOpenerService ) { const lastVersion = storageService.get(ProductContribution.KEY, StorageScope.GLOBAL, ''); // was there an update? if so, open release notes if (!environmentService.skipReleaseNotes && product.releaseNotesUrl && lastVersion && pkg.version !== lastVersion) { - showReleaseNotes(instantiationService, lastVersion) + showReleaseNotes(instantiationService, pkg.version) .then(undefined, () => { - const action = instantiationService.createInstance(OpenLatestReleaseNotesInBrowserAction); - const handle = notificationService.notify({ - severity: severity.Info, - message: nls.localize('read the release notes', "Welcome to {0} v{1}! Would you like to read the Release Notes?", product.nameLong, pkg.version), - actions: { primary: [action] } - }); - once(handle.onDidDispose)(() => action.dispose()); + notificationService.prompt( + severity.Info, + nls.localize('read the release notes', "Welcome to {0} v{1}! Would you like to read the Release Notes?", product.nameLong, pkg.version), + [{ + label: nls.localize('releaseNotes', "Release Notes"), + run: () => { + const uri = URI.parse(product.releaseNotesUrl); + openerService.open(uri); + } + }] + ); }); } // should we show the new license? if (product.licenseUrl && lastVersion && semver.satisfies(lastVersion, '<1.0.0') && semver.satisfies(pkg.version, '>=1.0.0')) { - notificationService.notify({ - severity: severity.Info, - message: nls.localize('licenseChanged', "Our license terms have changed, please click [here]({0}) to go through them.", product.licenseUrl), - }); + notificationService.info(nls.localize('licenseChanged', "Our license terms have changed, please click [here]({0}) to go through them.", product.licenseUrl)); } storageService.store(ProductContribution.KEY, pkg.version, StorageScope.GLOBAL); @@ -182,7 +176,7 @@ export class Win3264BitContribution implements IWorkbenchContribution { constructor( @IStorageService storageService: IStorageService, @IInstantiationService instantiationService: IInstantiationService, - @INotificationService private notificationService: INotificationService, + @INotificationService notificationService: INotificationService, @IWorkbenchEditorService editorService: IWorkbenchEditorService, @IEnvironmentService environmentService: IEnvironmentService ) { @@ -200,12 +194,18 @@ export class Win3264BitContribution implements IWorkbenchContribution { ? Win3264BitContribution.INSIDER_URL : Win3264BitContribution.URL; - const handle = this.notificationService.notify({ - severity: severity.Info, - message: nls.localize('64bitisavailable', "{0} for 64-bit Windows is now available! Click [here]({1}) to learn more.", product.nameShort, url), - actions: { secondary: [neverShowAgain.action] } - }); - once(handle.onDidDispose)(() => neverShowAgain.action.dispose()); + notificationService.prompt( + severity.Info, + nls.localize('64bitisavailable', "{0} for 64-bit Windows is now available! Click [here]({1}) to learn more.", product.nameShort, url), + [{ + label: nls.localize('neveragain', "Don't Show Again"), + isSecondary: true, + run: () => { + neverShowAgain.action.run(); + neverShowAgain.action.dispose(); + } + }] + ); } } @@ -223,7 +223,7 @@ class CommandAction extends Action { export class UpdateContribution implements IGlobalActivity { private static readonly showCommandsId = 'workbench.action.showCommands'; - private static readonly openSettingsId = 'workbench.action.openGlobalSettings'; + private static readonly openSettingsId = 'workbench.action.openSettings'; private static readonly openKeybindingsId = 'workbench.action.openGlobalKeybindings'; private static readonly openUserSnippets = 'workbench.action.openSnippets'; private static readonly selectColorThemeId = 'workbench.action.selectTheme'; @@ -330,16 +330,24 @@ export class UpdateContribution implements IGlobalActivity { return; } - const releaseNotesAction = this.instantiationService.createInstance(ShowReleaseNotesAction, update.productVersion); - const downloadAction = new Action('update.downloadNow', nls.localize('download now', "Download Now"), null, true, () => - this.updateService.downloadUpdate()); - - const handle = this.notificationService.notify({ - severity: severity.Info, - message: nls.localize('thereIsUpdateAvailable', "There is an available update."), - actions: { primary: [downloadAction, NotNowAction, releaseNotesAction] } - }); - once(handle.onDidDispose)(() => dispose(releaseNotesAction, downloadAction)); + this.notificationService.prompt( + severity.Info, + nls.localize('thereIsUpdateAvailable', "There is an available update."), + [{ + label: nls.localize('download now', "Download Now"), + run: () => this.updateService.downloadUpdate() + }, { + label: nls.localize('later', "Later"), + run: () => { } + }, { + label: nls.localize('releaseNotes', "Release Notes"), + run: () => { + const action = this.instantiationService.createInstance(ShowReleaseNotesAction, update.productVersion); + action.run(); + action.dispose(); + } + }] + ); } // windows fast updates @@ -348,16 +356,24 @@ export class UpdateContribution implements IGlobalActivity { return; } - const releaseNotesAction = this.instantiationService.createInstance(ShowReleaseNotesAction, update.productVersion); - const installUpdateAction = new Action('update.applyUpdate', nls.localize('installUpdate', "Install Update"), undefined, true, () => - this.updateService.applyUpdate()); - - const handle = this.notificationService.notify({ - severity: severity.Info, - message: nls.localize('updateAvailable', "There's an available update: {0} {1}", product.nameLong, update.productVersion), - actions: { primary: [installUpdateAction, NotNowAction, releaseNotesAction] } - }); - once(handle.onDidDispose)(() => dispose(installUpdateAction, releaseNotesAction)); + this.notificationService.prompt( + severity.Info, + nls.localize('updateAvailable', "There's an update available: {0} {1}", product.nameLong, update.productVersion), + [{ + label: nls.localize('installUpdate', "Install Update"), + run: () => this.updateService.applyUpdate() + }, { + label: nls.localize('later', "Later"), + run: () => { } + }, { + label: nls.localize('releaseNotes', "Release Notes"), + run: () => { + const action = this.instantiationService.createInstance(ShowReleaseNotesAction, update.productVersion); + action.run(); + action.dispose(); + } + }] + ); } // windows fast updates @@ -368,30 +384,44 @@ export class UpdateContribution implements IGlobalActivity { return; } - const handle = this.notificationService.notify({ - severity: severity.Info, - message: nls.localize('updateInstalling', "{0} {1} is being installed in the background, we'll let you know when it's done.", product.nameLong, update.productVersion), - actions: { secondary: [neverShowAgain.action] } - }); - once(handle.onDidDispose)(() => neverShowAgain.action.dispose()); + this.notificationService.prompt( + severity.Info, + nls.localize('updateInstalling', "{0} {1} is being installed in the background, we'll let you know when it's done.", product.nameLong, update.productVersion), + [{ + label: nls.localize('neveragain', "Don't Show Again"), + isSecondary: true, + run: () => { + neverShowAgain.action.run(); + neverShowAgain.action.dispose(); + } + }] + ); } // windows and mac private onUpdateReady(update: IUpdate): void { - if (!this.shouldShowNotification()) { + if (!isWindows && !this.shouldShowNotification()) { return; } - const releaseNotesAction = this.instantiationService.createInstance(ShowReleaseNotesAction, update.productVersion); - const applyUpdateAction = new Action('update.applyUpdate', nls.localize('updateNow', "Update Now"), undefined, true, () => - this.updateService.quitAndInstall()); - - const handle = this.notificationService.notify({ - severity: severity.Info, - message: nls.localize('updateAvailableAfterRestart', "{0} will be updated after it restarts.", product.nameLong), - actions: { primary: [applyUpdateAction, NotNowAction, releaseNotesAction] } - }); - once(handle.onDidDispose)(() => applyUpdateAction, releaseNotesAction); + this.notificationService.prompt( + severity.Info, + nls.localize('updateAvailableAfterRestart', "Restart {0} to apply the latest update.", product.nameLong), + [{ + label: nls.localize('updateNow', "Update Now"), + run: () => this.updateService.quitAndInstall() + }, { + label: nls.localize('later', "Later"), + run: () => { } + }, { + label: nls.localize('releaseNotes', "Release Notes"), + run: () => { + const action = this.instantiationService.createInstance(ShowReleaseNotesAction, update.productVersion); + action.run(); + action.dispose(); + } + }] + ); } private shouldShowNotification(): boolean { diff --git a/src/vs/workbench/parts/url/electron-browser/url.contribution.ts b/src/vs/workbench/parts/url/electron-browser/url.contribution.ts new file mode 100644 index 00000000000..edbcc48c138 --- /dev/null +++ b/src/vs/workbench/parts/url/electron-browser/url.contribution.ts @@ -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. + *--------------------------------------------------------------------------------------------*/ + +import { localize } from 'vs/nls'; +import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { Extensions as ActionExtensions, IWorkbenchActionRegistry } from 'vs/workbench/common/actions'; +import { IURLService } from 'vs/platform/url/common/url'; +import { IQuickOpenService } from 'vs/platform/quickOpen/common/quickOpen'; +import URI from 'vs/base/common/uri'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { Action } from 'vs/base/common/actions'; + +export class OpenUrlAction extends Action { + + static readonly ID = 'workbench.action.url.openUrl'; + static readonly LABEL = localize('openUrl', "Open URL"); + + constructor( + id: string, + label: string, + @IURLService private urlService: IURLService, + @IQuickOpenService private quickOpenService: IQuickOpenService, + ) { + super(id, label); + } + + async run(): TPromise { + const input = await this.quickOpenService.input({ prompt: 'URL to open' }); + const uri = URI.parse(input); + + this.urlService.open(uri); + } +} + +Registry.as(ActionExtensions.WorkbenchActions) + .registerWorkbenchAction(new SyncActionDescriptor(OpenUrlAction, OpenUrlAction.ID, OpenUrlAction.LABEL), 'OpenUrl', localize('developer', "Developer")); \ No newline at end of file diff --git a/src/vs/workbench/parts/watermark/electron-browser/watermark.ts b/src/vs/workbench/parts/watermark/electron-browser/watermark.ts index e750345e23c..44449b32df2 100644 --- a/src/vs/workbench/parts/watermark/electron-browser/watermark.ts +++ b/src/vs/workbench/parts/watermark/electron-browser/watermark.ts @@ -21,7 +21,7 @@ import { OpenRecentAction } from 'vs/workbench/electron-browser/actions'; import { GlobalNewUntitledFileAction } from 'vs/workbench/parts/files/electron-browser/fileActions'; import { OpenFolderAction, OpenFileFolderAction, OpenFileAction } from 'vs/workbench/browser/actions/workspaceActions'; import { ShowAllCommandsAction } from 'vs/workbench/parts/quickopen/browser/commandsHandler'; -import { Parts, IPartService, Dimension } from 'vs/workbench/services/part/common/partService'; +import { Parts, IPartService, IDimension } from 'vs/workbench/services/part/common/partService'; import { StartAction } from 'vs/workbench/parts/debug/browser/debugActions'; import { FindInFilesActionId } from 'vs/workbench/parts/search/common/constants'; import { ToggleTerminalAction } from 'vs/workbench/parts/terminal/electron-browser/terminalActions'; @@ -179,7 +179,7 @@ export class WatermarkContribution implements IWorkbenchContribution { update(); this.watermark.build(container.firstElementChild as HTMLElement, 0); this.toDispose.push(this.keybindingService.onDidUpdateKeybindings(update)); - this.toDispose.push(this.partService.onEditorLayout(({ height }: Dimension) => { + this.toDispose.push(this.partService.onEditorLayout(({ height }: IDimension) => { container.classList[height <= 478 ? 'add' : 'remove']('max-height-478px'); })); } diff --git a/src/vs/workbench/parts/html/electron-browser/baseWebviewEditor.ts b/src/vs/workbench/parts/webview/electron-browser/baseWebviewEditor.ts similarity index 92% rename from src/vs/workbench/parts/html/electron-browser/baseWebviewEditor.ts rename to src/vs/workbench/parts/webview/electron-browser/baseWebviewEditor.ts index c208bfaf888..8436046783d 100644 --- a/src/vs/workbench/parts/html/electron-browser/baseWebviewEditor.ts +++ b/src/vs/workbench/parts/webview/electron-browser/baseWebviewEditor.ts @@ -3,14 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Dimension } from 'vs/base/browser/dom'; +import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; - -import { IContextKey, RawContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; - -import { Webview } from './webview'; -import { Dimension } from 'vs/workbench/services/part/common/partService'; import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; +import { WebviewElement } from './webviewElement'; /** A context key that is set when a webview editor has focus. */ export const KEYBINDING_CONTEXT_WEBVIEWEDITOR_FOCUS = new RawContextKey('webviewEditorFocus', false); @@ -25,7 +23,7 @@ export const KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE = new RawContextKey< */ export abstract class BaseWebviewEditor extends BaseEditor { - protected _webview: Webview | undefined; + protected _webview: WebviewElement | undefined; protected contextKey: IContextKey; protected findWidgetVisible: IContextKey; protected findInputFocusContextKey: IContextKey; diff --git a/src/vs/workbench/parts/html/electron-browser/webview-pre.js b/src/vs/workbench/parts/webview/electron-browser/webview-pre.js similarity index 100% rename from src/vs/workbench/parts/html/electron-browser/webview-pre.js rename to src/vs/workbench/parts/webview/electron-browser/webview-pre.js diff --git a/src/vs/workbench/parts/webview/electron-browser/webview.contribution.ts b/src/vs/workbench/parts/webview/electron-browser/webview.contribution.ts index 4203fa20dff..c455d333963 100644 --- a/src/vs/workbench/parts/webview/electron-browser/webview.contribution.ts +++ b/src/vs/workbench/parts/webview/electron-browser/webview.contribution.ts @@ -3,15 +3,88 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IEditorRegistry, EditorDescriptor, Extensions as EditorExtensions } from 'vs/workbench/browser/editor'; -import { WebviewEditor } from './webviewEditor'; -import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { WebviewInput } from './webviewInput'; +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { localize } from 'vs/nls'; +import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { EditorDescriptor, Extensions as EditorExtensions, IEditorRegistry } from 'vs/workbench/browser/editor'; +import { Extensions as ActionExtensions, IWorkbenchActionRegistry } from 'vs/workbench/common/actions'; +import { Extensions as EditorInputExtensions, IEditorInputFactoryRegistry } from 'vs/workbench/common/editor'; +import { WebviewEditorInputFactory } from 'vs/workbench/parts/webview/electron-browser/webviewEditorInputFactory'; +import { KEYBINDING_CONTEXT_WEBVIEWEDITOR_FIND_WIDGET_INPUT_FOCUSED, KEYBINDING_CONTEXT_WEBVIEWEDITOR_FOCUS, KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE } from './baseWebviewEditor'; +import { HideWebViewEditorFindCommand, OpenWebviewDeveloperToolsAction, ReloadWebviewAction, ShowWebViewEditorFindTermCommand, ShowWebViewEditorFindWidgetCommand } from './webviewCommands'; +import { WebviewEditor } from './webviewEditor'; +import { WebviewEditorInput } from './webviewEditorInput'; +import { IWebviewEditorService, WebviewEditorService } from './webviewEditorService'; (Registry.as(EditorExtensions.Editors)).registerEditor(new EditorDescriptor( WebviewEditor, WebviewEditor.ID, localize('webview.editor.label', "webview editor")), - [new SyncDescriptor(WebviewInput)]); \ No newline at end of file + [new SyncDescriptor(WebviewEditorInput)]); + +Registry.as(EditorInputExtensions.EditorInputFactories).registerEditorInputFactory( + WebviewEditorInputFactory.ID, + WebviewEditorInputFactory); + +registerSingleton(IWebviewEditorService, WebviewEditorService); + + +const webviewDeveloperCategory = localize('developer', "Developer"); + +const actionRegistry = Registry.as(ActionExtensions.WorkbenchActions); + +const showNextFindWdigetCommand = new ShowWebViewEditorFindWidgetCommand({ + id: ShowWebViewEditorFindWidgetCommand.ID, + precondition: KEYBINDING_CONTEXT_WEBVIEWEDITOR_FOCUS, + kbOpts: { + primary: KeyMod.CtrlCmd | KeyCode.KEY_F + } +}); +KeybindingsRegistry.registerCommandAndKeybindingRule(showNextFindWdigetCommand.toCommandAndKeybindingRule(KeybindingsRegistry.WEIGHT.editorContrib())); + + +const showNextFindTermCommand = new ShowWebViewEditorFindTermCommand({ + id: 'editor.action.webvieweditor.showNextFindTerm', + precondition: KEYBINDING_CONTEXT_WEBVIEWEDITOR_FIND_WIDGET_INPUT_FOCUSED, + kbOpts: { + primary: KeyMod.Alt | KeyCode.DownArrow + } +}, true); +KeybindingsRegistry.registerCommandAndKeybindingRule(showNextFindTermCommand.toCommandAndKeybindingRule(KeybindingsRegistry.WEIGHT.editorContrib())); + +const showPreviousFindTermCommand = new ShowWebViewEditorFindTermCommand({ + id: 'editor.action.webvieweditor.showPreviousFindTerm', + precondition: KEYBINDING_CONTEXT_WEBVIEWEDITOR_FIND_WIDGET_INPUT_FOCUSED, + kbOpts: { + primary: KeyMod.Alt | KeyCode.UpArrow + } +}, false); +KeybindingsRegistry.registerCommandAndKeybindingRule(showPreviousFindTermCommand.toCommandAndKeybindingRule(KeybindingsRegistry.WEIGHT.editorContrib())); + + +const hideCommand = new HideWebViewEditorFindCommand({ + id: HideWebViewEditorFindCommand.ID, + precondition: ContextKeyExpr.and( + KEYBINDING_CONTEXT_WEBVIEWEDITOR_FOCUS, + KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE), + kbOpts: { + primary: KeyCode.Escape + } +}); +KeybindingsRegistry.registerCommandAndKeybindingRule(hideCommand.toCommandAndKeybindingRule(KeybindingsRegistry.WEIGHT.editorContrib())); + + +actionRegistry.registerWorkbenchAction( + new SyncActionDescriptor(OpenWebviewDeveloperToolsAction, OpenWebviewDeveloperToolsAction.ID, OpenWebviewDeveloperToolsAction.LABEL), + 'Webview Tools', + webviewDeveloperCategory); + +actionRegistry.registerWorkbenchAction( + new SyncActionDescriptor(ReloadWebviewAction, ReloadWebviewAction.ID, ReloadWebviewAction.LABEL), + 'Reload Webview', + webviewDeveloperCategory); \ No newline at end of file diff --git a/src/vs/workbench/parts/html/electron-browser/webview.html b/src/vs/workbench/parts/webview/electron-browser/webview.html similarity index 100% rename from src/vs/workbench/parts/html/electron-browser/webview.html rename to src/vs/workbench/parts/webview/electron-browser/webview.html diff --git a/src/vs/workbench/parts/html/electron-browser/webviewCommands.ts b/src/vs/workbench/parts/webview/electron-browser/webviewCommands.ts similarity index 70% rename from src/vs/workbench/parts/html/electron-browser/webviewCommands.ts rename to src/vs/workbench/parts/webview/electron-browser/webviewCommands.ts index eed860fc157..aefe8892396 100644 --- a/src/vs/workbench/parts/html/electron-browser/webviewCommands.ts +++ b/src/vs/workbench/parts/webview/electron-browser/webviewCommands.ts @@ -3,53 +3,34 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; - -import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { Command, ICommandOptions } from 'vs/editor/browser/editorExtensions'; -import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; import { Action } from 'vs/base/common/actions'; import { TPromise } from 'vs/base/common/winjs.base'; +import { Command, ICommandOptions } from 'vs/editor/browser/editorExtensions'; +import * as nls from 'vs/nls'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; import { BaseWebviewEditor } from './baseWebviewEditor'; export class ShowWebViewEditorFindWidgetCommand extends Command { public static readonly ID = 'editor.action.webvieweditor.showFind'; public runCommand(accessor: ServicesAccessor, args: any): void { - const webViewEditor = this.getWebViewEditor(accessor); + const webViewEditor = getActiveWebviewEditor(accessor); if (webViewEditor) { webViewEditor.showFind(); } - return null; - } - - private getWebViewEditor(accessor: ServicesAccessor): BaseWebviewEditor { - const workbenchEditorService = accessor.get(IWorkbenchEditorService); - const activeEditor = workbenchEditorService.getActiveEditor() as BaseWebviewEditor; - if (activeEditor.isWebviewEditor) { - return activeEditor; - } - return null; } } export class HideWebViewEditorFindCommand extends Command { - public static readonly Id = 'editor.action.webvieweditor.hideFind'; + public static readonly ID = 'editor.action.webvieweditor.hideFind'; public runCommand(accessor: ServicesAccessor, args: any): void { - const webViewEditor = this.getWebViewEditor(accessor); + const webViewEditor = getActiveWebviewEditor(accessor); if (webViewEditor) { webViewEditor.hideFind(); } } - - private getWebViewEditor(accessor: ServicesAccessor): BaseWebviewEditor { - const activeEditor = accessor.get(IWorkbenchEditorService).getActiveEditor() as BaseWebviewEditor; - if (activeEditor.isWebviewEditor) { - return activeEditor; - } - return null; - } } export class ShowWebViewEditorFindTermCommand extends Command { @@ -60,7 +41,7 @@ export class ShowWebViewEditorFindTermCommand extends Command { } public runCommand(accessor: ServicesAccessor, args: any): void { - const webViewEditor = this.getWebViewEditor(accessor); + const webViewEditor = getActiveWebviewEditor(accessor); if (webViewEditor) { if (this._next) { webViewEditor.showNextFindTerm(); @@ -69,14 +50,6 @@ export class ShowWebViewEditorFindTermCommand extends Command { } } } - - private getWebViewEditor(accessor: ServicesAccessor): BaseWebviewEditor { - const activeEditor = accessor.get(IWorkbenchEditorService).getActiveEditor() as BaseWebviewEditor; - if (activeEditor.isWebviewEditor) { - return activeEditor; - } - return null; - } } export class OpenWebviewDeveloperToolsAction extends Action { @@ -124,7 +97,13 @@ export class ReloadWebviewAction extends Action { private getVisibleWebviews() { return this.workbenchEditorService.getVisibleEditors() - .filter(c => c && (c as any).isWebviewEditor) - .map(e => e as BaseWebviewEditor); + .filter(editor => editor && (editor as BaseWebviewEditor).isWebviewEditor) + .map(editor => editor as BaseWebviewEditor); } +} + +function getActiveWebviewEditor(accessor: ServicesAccessor): BaseWebviewEditor | null { + const workbenchEditorService = accessor.get(IWorkbenchEditorService); + const activeEditor = workbenchEditorService.getActiveEditor() as BaseWebviewEditor; + return activeEditor.isWebviewEditor ? activeEditor : null; } \ No newline at end of file diff --git a/src/vs/workbench/parts/webview/electron-browser/webviewEditor.ts b/src/vs/workbench/parts/webview/electron-browser/webviewEditor.ts index ddbd2bc080d..aada9f588dc 100644 --- a/src/vs/workbench/parts/webview/electron-browser/webviewEditor.ts +++ b/src/vs/workbench/parts/webview/electron-browser/webviewEditor.ts @@ -3,24 +3,23 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as DOM from 'vs/base/browser/dom'; +import { Emitter, Event } from 'vs/base/common/event'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import URI from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; -import { IDisposable, } from 'vs/base/common/lifecycle'; -import { EditorOptions } from 'vs/workbench/common/editor'; -import { Position } from 'vs/platform/editor/common/editor'; -import { BaseWebviewEditor as BaseWebviewEditor, KEYBINDING_CONTEXT_WEBVIEWEDITOR_FOCUS, KEYBINDING_CONTEXT_WEBVIEWEDITOR_FIND_WIDGET_INPUT_FOCUSED, KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE } from 'vs/workbench/parts/html/electron-browser/baseWebviewEditor'; -import { Builder, Dimension } from 'vs/base/browser/builder'; -import { Webview } from 'vs/workbench/parts/html/electron-browser/webview'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { Position } from 'vs/platform/editor/common/editor'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { IPartService, Parts } from 'vs/workbench/services/part/common/partService'; -import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import * as DOM from 'vs/base/browser/dom'; -import { Event, Emitter } from 'vs/base/common/event'; -import { WebviewInput } from 'vs/workbench/parts/webview/electron-browser/webviewInput'; -import URI from 'vs/base/common/uri'; +import { EditorOptions } from 'vs/workbench/common/editor'; +import { WebviewEditorInput } from 'vs/workbench/parts/webview/electron-browser/webviewEditorInput'; +import { IPartService, Parts } from 'vs/workbench/services/part/common/partService'; +import { BaseWebviewEditor, KEYBINDING_CONTEXT_WEBVIEWEDITOR_FIND_WIDGET_INPUT_FOCUSED, KEYBINDING_CONTEXT_WEBVIEWEDITOR_FOCUS, KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE } from './baseWebviewEditor'; +import { WebviewElement } from './webviewElement'; export class WebviewEditor extends BaseWebviewEditor { @@ -29,10 +28,12 @@ export class WebviewEditor extends BaseWebviewEditor { private editorFrame: HTMLElement; private content: HTMLElement; private webviewContent: HTMLElement | undefined; - private readonly _onDidFocusWebview: Emitter; + private _webviewFocusTracker?: DOM.IFocusTracker; private _webviewFocusListenerDisposable?: IDisposable; + private readonly _onDidFocusWebview = new Emitter(); + constructor( @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @@ -43,18 +44,16 @@ export class WebviewEditor extends BaseWebviewEditor { @IWorkspaceContextService private readonly _contextService: IWorkspaceContextService ) { super(WebviewEditor.ID, telemetryService, themeService, _contextKeyService); - - this._onDidFocusWebview = new Emitter(); } - protected createEditor(parent: Builder): void { - this.editorFrame = parent.getHTMLElement(); + protected createEditor(parent: HTMLElement): void { + this.editorFrame = parent; this.content = document.createElement('div'); - parent.append(this.content); + parent.appendChild(this.content); } private doUpdateContainer() { - const webviewContainer = this.input && (this.input as WebviewInput).container; + const webviewContainer = this.input && (this.input as WebviewEditorInput).container; if (webviewContainer && webviewContainer.parentElement) { const frameRect = this.editorFrame.getBoundingClientRect(); const containerRect = webviewContainer.parentElement.getBoundingClientRect(); @@ -67,7 +66,7 @@ export class WebviewEditor extends BaseWebviewEditor { } } - public layout(dimension: Dimension): void { + public layout(dimension: DOM.Dimension): void { if (this._webview) { this.doUpdateContainer(); } @@ -103,14 +102,14 @@ export class WebviewEditor extends BaseWebviewEditor { } protected setEditorVisible(visible: boolean, position?: Position): void { - if (this.input && this.input instanceof WebviewInput) { + if (this.input && this.input instanceof WebviewEditorInput) { if (visible) { this.input.claimWebview(this); } else { this.input.releaseWebview(this); } - this.updateWebview(this.input as WebviewInput); + this.updateWebview(this.input as WebviewEditorInput); } if (this.webviewContent) { @@ -126,7 +125,7 @@ export class WebviewEditor extends BaseWebviewEditor { } public clearInput() { - if (this.input && this.input instanceof WebviewInput) { + if (this.input && this.input instanceof WebviewEditorInput) { this.input.releaseWebview(this); } @@ -136,24 +135,24 @@ export class WebviewEditor extends BaseWebviewEditor { super.clearInput(); } - async setInput(input: WebviewInput, options: EditorOptions): TPromise { + async setInput(input: WebviewEditorInput, options: EditorOptions): TPromise { if (this.input && this.input.matches(input)) { return undefined; } if (this.input) { - (this.input as WebviewInput).releaseWebview(this); + (this.input as WebviewEditorInput).releaseWebview(this); this._webview = undefined; this.webviewContent = undefined; } - await super.setInput(input, options); - input.onDidChangePosition(this.position); + await input.resolve(); + await input.updatePosition(this.position); this.updateWebview(input); } - private updateWebview(input: WebviewInput) { + private updateWebview(input: WebviewEditorInput) { const webview = this.getWebview(input); input.claimWebview(this); webview.options = { @@ -163,7 +162,7 @@ export class WebviewEditor extends BaseWebviewEditor { useSameOriginForRoot: false, localResourceRoots: input.options.localResourceRoots || this.getDefaultLocalResourceRoots() }; - input.setHtml(input.html); + input.html = input.html; if (this.webviewContent) { this.webviewContent.style.visibility = 'visible'; @@ -174,29 +173,27 @@ export class WebviewEditor extends BaseWebviewEditor { private getDefaultLocalResourceRoots(): URI[] { const rootPaths = this._contextService.getWorkspace().folders.map(x => x.uri); - if ((this.input as WebviewInput).extensionFolderPath) { - rootPaths.push((this.input as WebviewInput).extensionFolderPath); + if ((this.input as WebviewEditorInput).extensionFolderPath) { + rootPaths.push((this.input as WebviewEditorInput).extensionFolderPath); } return rootPaths; } - private getWebview(input: WebviewInput): Webview { + private getWebview(input: WebviewEditorInput): WebviewElement { if (this._webview) { return this._webview; } this.webviewContent = input.container; + + this.trackFocus(); + const existing = input.webview; if (existing) { this._webview = existing; return existing; } - this._webviewFocusTracker = DOM.trackFocus(this.webviewContent); - this._webviewFocusListenerDisposable = this._webviewFocusTracker.onDidFocus(() => { - this._onDidFocusWebview.fire(); - }); - if (input.options.enableFindWidget) { this._contextKeyService = this._contextKeyService.createScoped(this.webviewContent); this.contextKey = KEYBINDING_CONTEXT_WEBVIEWEDITOR_FOCUS.bindTo(this._contextKeyService); @@ -204,7 +201,7 @@ export class WebviewEditor extends BaseWebviewEditor { this.findWidgetVisible = KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE.bindTo(this._contextKeyService); } - this._webview = new Webview( + this._webview = new WebviewElement( this._partService.getContainer(Parts.EDITOR_PART), this.themeService, this._environmentService, @@ -227,5 +224,19 @@ export class WebviewEditor extends BaseWebviewEditor { this.doUpdateContainer(); return this._webview; } + + private trackFocus() { + if (this._webviewFocusTracker) { + this._webviewFocusTracker.dispose(); + } + if (this._webviewFocusListenerDisposable) { + this._webviewFocusListenerDisposable.dispose(); + } + + this._webviewFocusTracker = DOM.trackFocus(this.webviewContent); + this._webviewFocusListenerDisposable = this._webviewFocusTracker.onDidFocus(() => { + this._onDidFocusWebview.fire(); + }); + } } diff --git a/src/vs/workbench/parts/webview/electron-browser/webviewInput.ts b/src/vs/workbench/parts/webview/electron-browser/webviewEditorInput.ts similarity index 74% rename from src/vs/workbench/parts/webview/electron-browser/webviewInput.ts rename to src/vs/workbench/parts/webview/electron-browser/webviewEditorInput.ts index 0d925f614b7..516b46d3d5b 100644 --- a/src/vs/workbench/parts/webview/electron-browser/webviewInput.ts +++ b/src/vs/workbench/parts/webview/electron-browser/webviewEditorInput.ts @@ -3,71 +3,61 @@ * 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 { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { EditorInput, EditorModel } from 'vs/workbench/common/editor'; -import { IEditorModel, Position, IEditorInput } from 'vs/platform/editor/common/editor'; -import { Webview } from 'vs/workbench/parts/html/electron-browser/webview'; -import { IPartService, Parts } from 'vs/workbench/services/part/common/partService'; -import * as vscode from 'vscode'; import URI from 'vs/base/common/uri'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { IEditorInput, IEditorModel, Position } from 'vs/platform/editor/common/editor'; +import { EditorInput, EditorModel } from 'vs/workbench/common/editor'; +import { IPartService, Parts } from 'vs/workbench/services/part/common/partService'; +import { WebviewEvents, WebviewInputOptions, WebviewReviver } from './webviewEditorService'; +import { WebviewElement } from './webviewElement'; -export interface WebviewEvents { - onMessage?(message: any): void; - onDidChangePosition?(newPosition: Position): void; - onDispose?(): void; - onDidClickLink?(link: URI, options: vscode.WebviewOptions): void; -} -export interface WebviewInputOptions extends vscode.WebviewOptions { - tryRestoreScrollPosition?: boolean; -} - -export class WebviewInput extends EditorInput { +export class WebviewEditorInput extends EditorInput { private static handlePool = 0; + public static readonly typeId = 'workbench.editors.webviewInput'; + private _name: string; private _options: WebviewInputOptions; - private _html: string; + private _html: string = ''; private _currentWebviewHtml: string = ''; - private _events: WebviewEvents | undefined; + public _events: WebviewEvents | undefined; private _container: HTMLElement; - private _webview: Webview | undefined; + private _webview: WebviewElement | undefined; private _webviewOwner: any; private _webviewDisposables: IDisposable[] = []; private _position?: Position; private _scrollYPercentage: number = 0; + private _state: any; + + private _revived: boolean = false; + public readonly extensionFolderPath: URI | undefined; constructor( + public readonly viewType: string, name: string, options: WebviewInputOptions, - html: string, + state: any, events: WebviewEvents, - partService: IPartService, - extensionFolderPath?: string + extensionFolderPath: string | undefined, + public readonly reviver: WebviewReviver | undefined, + @IPartService private readonly _partService: IPartService, ) { super(); this._name = name; this._options = options; - this._html = html; this._events = events; + this._state = state; if (extensionFolderPath) { this.extensionFolderPath = URI.file(extensionFolderPath); } - - const id = WebviewInput.handlePool++; - this._container = document.createElement('div'); - this._container.id = `webview-${id}`; - - partService.getContainer(Parts.EDITOR_PART).appendChild(this._container); } public getTypeId(): string { - return 'webview'; + return WebviewEditorInput.typeId; } public dispose() { @@ -119,7 +109,7 @@ export class WebviewInput extends EditorInput { return this._html; } - public setHtml(value: string): void { + public set html(value: string) { if (value === this._currentWebviewHtml) { return; } @@ -132,6 +122,14 @@ export class WebviewInput extends EditorInput { } } + public get state(): any { + return this._state; + } + + public set state(value: any) { + this._state = value; + } + public get options(): WebviewInputOptions { return this._options; } @@ -141,6 +139,11 @@ export class WebviewInput extends EditorInput { } public resolve(refresh?: boolean): TPromise { + if (this.reviver && !this._revived) { + this._revived = true; + return this.reviver.reviveWebview(this).then(() => new EditorModel()); + } + return TPromise.as(new EditorModel()); } @@ -149,14 +152,20 @@ export class WebviewInput extends EditorInput { } public get container(): HTMLElement { + if (!this._container) { + const id = WebviewEditorInput.handlePool++; + this._container = document.createElement('div'); + this._container.id = `webview-${id}`; + this._partService.getContainer(Parts.EDITOR_PART).appendChild(this._container); + } return this._container; } - public get webview(): Webview | undefined { + public get webview(): WebviewElement | undefined { return this._webview; } - public set webview(value: Webview) { + public set webview(value: WebviewElement) { this._webviewDisposables = dispose(this._webviewDisposables); this._webview = value; @@ -215,10 +224,7 @@ export class WebviewInput extends EditorInput { this._currentWebviewHtml = ''; } - public onDidChangePosition(position: Position) { - if (this._events && this._events.onDidChangePosition) { - this._events.onDidChangePosition(position); - } + public updatePosition(position: Position): void { this._position = position; } } diff --git a/src/vs/workbench/parts/webview/electron-browser/webviewEditorInputFactory.ts b/src/vs/workbench/parts/webview/electron-browser/webviewEditorInputFactory.ts new file mode 100644 index 00000000000..476db7789b1 --- /dev/null +++ b/src/vs/workbench/parts/webview/electron-browser/webviewEditorInputFactory.ts @@ -0,0 +1,57 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IEditorInputFactory } from 'vs/workbench/common/editor'; +import { WebviewEditorInput } from './webviewEditorInput'; +import { IWebviewEditorService, WebviewInputOptions } from './webviewEditorService'; + +interface SerializedWebview { + readonly viewType: string; + readonly title: string; + readonly options: WebviewInputOptions; + readonly extensionFolderPath: string; + readonly state: any; +} + +export class WebviewEditorInputFactory implements IEditorInputFactory { + + public static readonly ID = WebviewEditorInput.typeId; + + public constructor( + @IWebviewEditorService private readonly _webviewService: IWebviewEditorService + ) { } + + public serialize( + input: WebviewEditorInput + ): string { + // Has no state, don't revive + if (!input.state) { + return null; + } + + // Only attempt revival if we may have a reviver + if (!this._webviewService.canRevive(input) && !input.reviver) { + return null; + } + + const data: SerializedWebview = { + viewType: input.viewType, + title: input.getName(), + options: input.options, + extensionFolderPath: input.extensionFolderPath.fsPath, + state: input.state + }; + return JSON.stringify(data); + } + + public deserialize( + instantiationService: IInstantiationService, + serializedEditorInput: string + ): WebviewEditorInput { + const data: SerializedWebview = JSON.parse(serializedEditorInput); + return this._webviewService.reviveWebview(data.viewType, data.title, data.state, data.options, data.extensionFolderPath); + } +} diff --git a/src/vs/workbench/parts/webview/electron-browser/webviewEditorService.ts b/src/vs/workbench/parts/webview/electron-browser/webviewEditorService.ts new file mode 100644 index 00000000000..1d0f11200ca --- /dev/null +++ b/src/vs/workbench/parts/webview/electron-browser/webviewEditorService.ts @@ -0,0 +1,178 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import URI from 'vs/base/common/uri'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { Position } from 'vs/platform/editor/common/editor'; +import { IInstantiationService, createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService'; +import * as vscode from 'vscode'; +import { WebviewEditorInput } from './webviewEditorInput'; + +export const IWebviewEditorService = createDecorator('webviewEditorService'); + +export interface IWebviewEditorService { + _serviceBrand: any; + + createWebview( + viewType: string, + title: string, + column: Position, + options: WebviewInputOptions, + extensionFolderPath: string, + events: WebviewEvents + ): WebviewEditorInput; + + reviveWebview( + viewType: string, + title: string, + state: any, + options: WebviewInputOptions, + extensionFolderPath: string + ): WebviewEditorInput; + + revealWebview( + webview: WebviewEditorInput, + column: Position | undefined + ): void; + + registerReviver( + viewType: string, + reviver: WebviewReviver + ): IDisposable; + + canRevive( + input: WebviewEditorInput + ): boolean; +} + +export interface WebviewReviver { + canRevive( + webview: WebviewEditorInput + ): boolean; + + reviveWebview( + webview: WebviewEditorInput + ): TPromise; +} + +export interface WebviewEvents { + onMessage?(message: any): void; + onDidChangePosition?(newPosition: Position): void; + onDispose?(): void; + onDidClickLink?(link: URI, options: vscode.WebviewOptions): void; +} + +export interface WebviewInputOptions extends vscode.WebviewOptions, vscode.WebviewPanelOptions { + tryRestoreScrollPosition?: boolean; +} + +export class WebviewEditorService implements IWebviewEditorService { + _serviceBrand: any; + + private readonly _revivers = new Map(); + private _awaitingRevival: { input: WebviewEditorInput, resolve: (x: any) => void }[] = []; + + constructor( + @IWorkbenchEditorService private readonly _editorService: IWorkbenchEditorService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IEditorGroupService private readonly _editorGroupService: IEditorGroupService, + ) { } + + createWebview( + viewType: string, + title: string, + column: Position, + options: vscode.WebviewOptions, + extensionFolderPath: string, + events: WebviewEvents + ): WebviewEditorInput { + const webviewInput = this._instantiationService.createInstance(WebviewEditorInput, viewType, title, options, {}, events, extensionFolderPath, undefined); + this._editorService.openEditor(webviewInput, { pinned: true }, column); + return webviewInput; + } + + revealWebview( + webview: WebviewEditorInput, + column: Position | undefined + ): void { + if (typeof column === 'undefined' || webview.position === column) { + this._editorService.openEditor(webview, { preserveFocus: false }, column); + } else { + this._editorGroupService.moveEditor(webview, webview.position, column, { preserveFocus: false }); + } + } + + reviveWebview( + viewType: string, + title: string, + state: any, + options: WebviewInputOptions, + extensionFolderPath: string + ): WebviewEditorInput { + const webviewInput = this._instantiationService.createInstance(WebviewEditorInput, viewType, title, options, state, {}, extensionFolderPath, { + canRevive: (webview) => { + return true; + }, + reviveWebview: async (webview: WebviewEditorInput): TPromise => { + const didRevive = await this.tryRevive(webview); + if (didRevive) { + return; + } + // A reviver may not be registered yet. Put into queue and resolve promise when we can revive + let resolve: (value: void) => void; + const promise = new TPromise(r => { resolve = r; }); + this._awaitingRevival.push({ input: webview, resolve }); + return promise; + } + }); + + return webviewInput; + } + + registerReviver( + viewType: string, + reviver: WebviewReviver + ): IDisposable { + if (this._revivers.has(viewType)) { + throw new Error(`Reviver for '${viewType}' already registered`); + } + + this._revivers.set(viewType, reviver); + + // Resolve any pending views + const toRevive = this._awaitingRevival.filter(x => x.input.viewType === viewType); + this._awaitingRevival = this._awaitingRevival.filter(x => x.input.viewType !== viewType); + + for (const input of toRevive) { + reviver.reviveWebview(input.input).then(() => input.resolve(void 0)); + } + + return toDisposable(() => { + this._revivers.delete(viewType); + }); + } + + canRevive( + webview: WebviewEditorInput + ): boolean { + const viewType = webview.viewType; + return this._revivers.has(viewType) && this._revivers.get(viewType).canRevive(webview); + } + + private async tryRevive( + webview: WebviewEditorInput + ): TPromise { + const reviver = this._revivers.get(webview.viewType); + if (!reviver) { + return false; + } + + await reviver.reviveWebview(webview); + return true; + } +} \ No newline at end of file diff --git a/src/vs/workbench/parts/html/electron-browser/webview.ts b/src/vs/workbench/parts/webview/electron-browser/webviewElement.ts similarity index 96% rename from src/vs/workbench/parts/html/electron-browser/webview.ts rename to src/vs/workbench/parts/webview/electron-browser/webviewElement.ts index d33a1c750f8..7a7836459dd 100644 --- a/src/vs/workbench/parts/html/electron-browser/webview.ts +++ b/src/vs/workbench/parts/webview/electron-browser/webviewElement.ts @@ -3,18 +3,18 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import URI from 'vs/base/common/uri'; +import { addClass, addDisposableListener } from 'vs/base/browser/dom'; +import { Emitter, Event } from 'vs/base/common/event'; 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 { editorBackground, editorForeground, textLinkForeground } from 'vs/platform/theme/common/colorRegistry'; -import { ITheme, LIGHT, DARK, IThemeService } 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'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { nativeSep } from 'vs/base/common/paths'; import { startsWith } from 'vs/base/common/strings'; +import URI from 'vs/base/common/uri'; +import { IContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { editorBackground, editorForeground, textLinkForeground } from 'vs/platform/theme/common/colorRegistry'; +import { DARK, ITheme, IThemeService, LIGHT } from 'vs/platform/theme/common/themeService'; +import { WebviewFindWidget } from './webviewFindWidget'; export interface WebviewOptions { readonly allowScripts?: boolean; @@ -22,10 +22,10 @@ export interface WebviewOptions { readonly svgWhiteList?: string[]; readonly enableWrappedPostMessage?: boolean; readonly useSameOriginForRoot?: boolean; - readonly localResourceRoots?: URI[]; + readonly localResourceRoots?: ReadonlyArray; } -export class Webview { +export class WebviewElement { private readonly _webview: Electron.WebviewTag; private _ready: Promise; private _disposables: IDisposable[] = []; @@ -421,7 +421,7 @@ namespace ApiThemeClassName { function registerFileProtocol( contents: Electron.WebContents, protocol: string, - getRoots: () => URI[] + getRoots: () => ReadonlyArray ) { contents.session.protocol.registerFileProtocol(protocol, (request, callback: any) => { const requestPath = URI.parse(request.url).path; @@ -432,7 +432,8 @@ function registerFileProtocol( return; } } - callback({ error: 'Cannot load resource outside of protocol root' }); + console.error('Webview: Cannot load resource outside of protocol root'); + callback({ error: -10 /* ACCESS_DENIED: https://cs.chromium.org/chromium/src/net/base/net_error_list.h */ }); }, (error) => { if (error) { console.error('Failed to register protocol ' + protocol); diff --git a/src/vs/workbench/parts/html/electron-browser/webviewFindWidget.ts b/src/vs/workbench/parts/webview/electron-browser/webviewFindWidget.ts similarity index 94% rename from src/vs/workbench/parts/html/electron-browser/webviewFindWidget.ts rename to src/vs/workbench/parts/webview/electron-browser/webviewFindWidget.ts index f304d2f56eb..13ca4b99ce4 100644 --- a/src/vs/workbench/parts/html/electron-browser/webviewFindWidget.ts +++ b/src/vs/workbench/parts/webview/electron-browser/webviewFindWidget.ts @@ -5,13 +5,13 @@ import { SimpleFindWidget } from 'vs/editor/contrib/find/simpleFindWidget'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; -import { Webview } from './webview'; +import { WebviewElement } from './webviewElement'; export class WebviewFindWidget extends SimpleFindWidget { constructor( @IContextViewService contextViewService: IContextViewService, - private readonly webview: Webview + private readonly webview: WebviewElement ) { super(contextViewService); } diff --git a/src/vs/workbench/parts/welcome/gettingStarted/electron-browser/gettingStarted.ts b/src/vs/workbench/parts/welcome/gettingStarted/electron-browser/gettingStarted.ts index a2948effde7..c86b0a64c73 100644 --- a/src/vs/workbench/parts/welcome/gettingStarted/electron-browser/gettingStarted.ts +++ b/src/vs/workbench/parts/welcome/gettingStarted/electron-browser/gettingStarted.ts @@ -25,11 +25,20 @@ export class GettingStarted implements IWorkbenchContribution { ) { this.appName = product.nameLong; - /* do not open a browser when we run an extension or --skip-getting-started is provided */ - if (product.welcomePage && !environmentService.isExtensionDevelopment && !environmentService.skipGettingStarted) { - this.welcomePageURL = product.welcomePage; - this.handleWelcome(); + if (!product.welcomePage) { + return; } + + if (environmentService.skipGettingStarted) { + return; + } + + if (environmentService.isExtensionDevelopment) { + return; + } + + this.welcomePageURL = product.welcomePage; + this.handleWelcome(); } private getUrl(telemetryInfo: ITelemetryInfo): string { diff --git a/src/vs/workbench/parts/welcome/gettingStarted/electron-browser/telemetryOptOut.ts b/src/vs/workbench/parts/welcome/gettingStarted/electron-browser/telemetryOptOut.ts index 072ca1fbb84..49a9d309bbf 100644 --- a/src/vs/workbench/parts/welcome/gettingStarted/electron-browser/telemetryOptOut.ts +++ b/src/vs/workbench/parts/welcome/gettingStarted/electron-browser/telemetryOptOut.ts @@ -13,7 +13,7 @@ import { INotificationService, Severity } from 'vs/platform/notification/common/ import URI from 'vs/base/common/uri'; import { localize } from 'vs/nls'; import { onUnexpectedError } from 'vs/base/common/errors'; -import { IWindowService } from 'vs/platform/windows/common/windows'; +import { IWindowService, IWindowsService } from 'vs/platform/windows/common/windows'; export class TelemetryOptOut implements IWorkbenchContribution { @@ -24,13 +24,17 @@ export class TelemetryOptOut implements IWorkbenchContribution { @IOpenerService openerService: IOpenerService, @INotificationService notificationService: INotificationService, @IWindowService windowService: IWindowService, + @IWindowsService windowsService: IWindowsService, @ITelemetryService telemetryService: ITelemetryService ) { if (!product.telemetryOptOutUrl || storageService.get(TelemetryOptOut.TELEMETRY_OPT_OUT_SHOWN)) { return; } - windowService.isFocused().then(focused => { - if (!focused) { + Promise.all([ + windowService.isFocused(), + windowsService.getWindowCount() + ]).then(([focused, count]) => { + if (!focused && count > 1) { return null; } storageService.store(TelemetryOptOut.TELEMETRY_OPT_OUT_SHOWN, true); @@ -39,8 +43,15 @@ export class TelemetryOptOut implements IWorkbenchContribution { const privacyUrl = product.privacyStatementUrl || product.telemetryOptOutUrl; const optOutNotice = localize('telemetryOptOut.optOutNotice', "Help improve VS Code by allowing Microsoft to collect usage data. Read our [privacy statement]({0}) and learn how to [opt out]({1}).", privacyUrl, optOutUrl); const optInNotice = localize('telemetryOptOut.optInNotice', "Help improve VS Code by allowing Microsoft to collect usage data. Read our [privacy statement]({0}) and learn how to [opt in]({1}).", privacyUrl, optOutUrl); - return notificationService.prompt(Severity.Info, telemetryService.isOptedIn ? optOutNotice : optInNotice, [localize('telemetryOptOut.readMore', "Read More")]) - .then(() => openerService.open(URI.parse(optOutUrl))); + + notificationService.prompt( + Severity.Info, + telemetryService.isOptedIn ? optOutNotice : optInNotice, + [{ + label: localize('telemetryOptOut.readMore', "Read More"), + run: () => openerService.open(URI.parse(optOutUrl)) + }] + ); }) .then(null, onUnexpectedError); } diff --git a/src/vs/workbench/parts/welcome/overlay/browser/welcomeOverlay.css b/src/vs/workbench/parts/welcome/overlay/browser/welcomeOverlay.css index 50cf77eafad..bc843e58ab3 100644 --- a/src/vs/workbench/parts/welcome/overlay/browser/welcomeOverlay.css +++ b/src/vs/workbench/parts/welcome/overlay/browser/welcomeOverlay.css @@ -97,8 +97,16 @@ left: 45px; } +.monaco-workbench > .welcomeOverlay > .key.notifications { + position: absolute; + bottom: 25px; + right: 16px; +} + .monaco-workbench > .welcomeOverlay > .key.problems > .label, -.monaco-workbench > .welcomeOverlay > .key.problems > .shortcut { +.monaco-workbench > .welcomeOverlay > .key.problems > .shortcut, +.monaco-workbench > .welcomeOverlay > .key.notifications > .label, +.monaco-workbench > .welcomeOverlay > .key.notifications > .shortcut { vertical-align: super; } diff --git a/src/vs/workbench/parts/welcome/overlay/browser/welcomeOverlay.ts b/src/vs/workbench/parts/welcome/overlay/browser/welcomeOverlay.ts index 75313c3e266..9e0118794aa 100644 --- a/src/vs/workbench/parts/welcome/overlay/browser/welcomeOverlay.ts +++ b/src/vs/workbench/parts/welcome/overlay/browser/welcomeOverlay.ts @@ -91,6 +91,13 @@ const keys: Key[] = [ label: localize('welcomeOverlay.commandPalette', "Find and run all commands"), command: ShowAllCommandsAction.ID }, + { + id: 'notifications', + arrow: '⤵', + arrowLast: true, + label: localize('welcomeOverlay.notifications', "Show notifications"), + command: 'notifications.showList' + } ]; const OVERLAY_VISIBLE = new RawContextKey('interfaceOverviewVisible', false); diff --git a/src/vs/workbench/parts/welcome/page/electron-browser/vs_code_welcome_page.ts b/src/vs/workbench/parts/welcome/page/electron-browser/vs_code_welcome_page.ts index c026520816d..27e73025e43 100644 --- a/src/vs/workbench/parts/welcome/page/electron-browser/vs_code_welcome_page.ts +++ b/src/vs/workbench/parts/welcome/page/electron-browser/vs_code_welcome_page.ts @@ -57,7 +57,7 @@ export default () => ` .replace('{0}', ``) .replace('{1}', `${escape(localize('welcomePage.moreExtensions', "more"))}`)} -
  • 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 290a2b95ac3..21f183d2458 100644 --- a/src/vs/workbench/parts/welcome/page/electron-browser/welcomePage.ts +++ b/src/vs/workbench/parts/welcome/page/electron-browser/welcomePage.ts @@ -427,9 +427,12 @@ class WelcomePage { }); }); - this.notificationService.prompt(Severity.Info, strings.reloadAfterInstall.replace('{0}', extensionSuggestion.name), [localize('ok', "OK"), localize('details', "Details")]).then(choice => { - switch (choice) { - case 0 /* OK */: + this.notificationService.prompt( + Severity.Info, + strings.reloadAfterInstall.replace('{0}', extensionSuggestion.name), + [{ + label: localize('ok', "OK"), + run: () => { const messageDelay = TPromise.timeout(300); messageDelay.then(() => { this.notificationService.info(strings.installing.replace('{0}', extensionSuggestion.name)); @@ -491,8 +494,10 @@ class WelcomePage { }); this.notificationService.error(err); }); - break; - case 1 /* Details */: + } + }, { + label: localize('details', "Details"), + run: () => { /* __GDPR__FRAGMENT__ "WelcomePageDetails-1" : { "from" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, @@ -506,9 +511,9 @@ class WelcomePage { this.extensionsWorkbenchService.queryGallery({ names: [extensionSuggestion.id] }) .then(result => this.extensionsWorkbenchService.open(result.firstPage[0])) .then(null, onUnexpectedError); - break; - } - }); + } + }] + ); }).then(null, err => { /* __GDPR__FRAGMENT__ "WelcomePageInstalled-6" : { diff --git a/src/vs/workbench/parts/welcome/walkThrough/electron-browser/walkThroughPart.ts b/src/vs/workbench/parts/welcome/walkThrough/electron-browser/walkThroughPart.ts index cf45387bede..8198c3ea31c 100644 --- a/src/vs/workbench/parts/welcome/walkThrough/electron-browser/walkThroughPart.ts +++ b/src/vs/workbench/parts/welcome/walkThrough/electron-browser/walkThroughPart.ts @@ -11,9 +11,8 @@ import { ScrollbarVisibility } from 'vs/base/common/scrollable'; import * as strings from 'vs/base/common/strings'; import URI from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; -import { $, Dimension, Builder } from 'vs/base/browser/builder'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { EditorOptions } from 'vs/workbench/common/editor'; +import { EditorOptions, EditorViewStateMemento } from 'vs/workbench/common/editor'; import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { WalkThroughInput } from 'vs/workbench/parts/welcome/walkThrough/node/walkThroughInput'; @@ -40,6 +39,7 @@ import { UILabelProvider } from 'vs/base/common/keybindingLabels'; import { OS, OperatingSystem } from 'vs/base/common/platform'; import { deepClone } from 'vs/base/common/objects'; import { INotificationService } from 'vs/platform/notification/common/notification'; +import { Dimension, size } from 'vs/base/browser/dom'; export const WALK_THROUGH_FOCUS = new RawContextKey('interactivePlaygroundFocus', false); @@ -55,12 +55,6 @@ interface IWalkThroughEditorViewState { viewState: IViewState; } -interface IWalkThroughEditorViewStates { - 0?: IWalkThroughEditorViewState; - 1?: IWalkThroughEditorViewState; - 2?: IWalkThroughEditorViewState; -} - class WalkThroughCodeEditor extends CodeEditor { constructor( @@ -71,9 +65,10 @@ class WalkThroughCodeEditor extends CodeEditor { @ICodeEditorService codeEditorService: ICodeEditorService, @ICommandService commandService: ICommandService, @IContextKeyService contextKeyService: IContextKeyService, - @IThemeService themeService: IThemeService + @IThemeService themeService: IThemeService, + @INotificationService notificationService: INotificationService, ) { - super(domElement, options, instantiationService, codeEditorService, commandService, contextKeyService, themeService); + super(domElement, options, instantiationService, codeEditorService, commandService, contextKeyService, themeService, notificationService); } getTelemetryData() { @@ -91,6 +86,7 @@ export class WalkThroughPart extends BaseEditor { private scrollbar: DomScrollableElement; private editorFocus: IContextKey; private size: Dimension; + private editorViewStateMemento: EditorViewStateMemento; constructor( @ITelemetryService telemetryService: ITelemetryService, @@ -99,18 +95,17 @@ export class WalkThroughPart extends BaseEditor { @IInstantiationService private instantiationService: IInstantiationService, @IOpenerService private openerService: IOpenerService, @IKeybindingService private keybindingService: IKeybindingService, - @IStorageService private storageService: IStorageService, + @IStorageService storageService: IStorageService, @IContextKeyService private contextKeyService: IContextKeyService, @IConfigurationService private configurationService: IConfigurationService, @INotificationService private notificationService: INotificationService ) { super(WalkThroughPart.ID, telemetryService, themeService); this.editorFocus = WALK_THROUGH_FOCUS.bindTo(this.contextKeyService); + this.editorViewStateMemento = new EditorViewStateMemento(this.getMemento(storageService, Scope.WORKSPACE), WALK_THROUGH_EDITOR_VIEW_STATE_PREFERENCE_KEY); } - createEditor(parent: Builder): void { - const container = parent.getHTMLElement(); - + createEditor(container: HTMLElement): void { this.content = document.createElement('div'); this.content.tabIndex = 0; this.content.style.outlineStyle = 'none'; @@ -214,9 +209,9 @@ export class WalkThroughPart extends BaseEditor { return uri.with({ query: JSON.stringify(query) }); } - layout(size: Dimension): void { - this.size = size; - $(this.content).style({ height: `${size.height}px`, width: `${size.width}px` }); + layout(dimension: Dimension): void { + this.size = dimension; + size(this.content, dimension.width, dimension.height); this.updateSizeClasses(); this.contentDisposables.forEach(disposable => { if (disposable instanceof CodeEditor) { @@ -281,7 +276,7 @@ export class WalkThroughPart extends BaseEditor { } if (this.input instanceof WalkThroughInput) { - this.saveTextEditorViewState(this.input.getResource()); + this.saveTextEditorViewState(this.input); } this.contentDisposables = dispose(this.contentDisposables); @@ -302,7 +297,7 @@ export class WalkThroughPart extends BaseEditor { input.onReady(this.content.firstElementChild as HTMLElement); } this.scrollbar.scanDomNode(); - this.loadTextEditorViewState(input.getResource()); + this.loadTextEditorViewState(input); this.updatedScrollPosition(); return; } @@ -432,7 +427,7 @@ export class WalkThroughPart extends BaseEditor { input.onReady(innerContent); } this.scrollbar.scanDomNode(); - this.loadTextEditorViewState(input.getResource()); + this.loadTextEditorViewState(input); this.updatedScrollPosition(); }); } @@ -496,61 +491,46 @@ export class WalkThroughPart extends BaseEditor { }); } - private saveTextEditorViewState(resource: URI): void { - const memento = this.getMemento(this.storageService, Scope.WORKSPACE); - let editorViewStateMemento: { [key: string]: { [position: number]: IWalkThroughEditorViewState } } = memento[WALK_THROUGH_EDITOR_VIEW_STATE_PREFERENCE_KEY]; - if (!editorViewStateMemento) { - editorViewStateMemento = Object.create(null); - memento[WALK_THROUGH_EDITOR_VIEW_STATE_PREFERENCE_KEY] = editorViewStateMemento; - } - + private saveTextEditorViewState(input: WalkThroughInput): void { const scrollPosition = this.scrollbar.getScrollPosition(); - const editorViewState: IWalkThroughEditorViewState = { + + this.editorViewStateMemento.saveState(input, this.position, { viewState: { scrollTop: scrollPosition.scrollTop, scrollLeft: scrollPosition.scrollLeft } - }; - - let fileViewState = editorViewStateMemento[resource.toString()]; - if (!fileViewState) { - fileViewState = Object.create(null); - editorViewStateMemento[resource.toString()] = fileViewState; - } - - if (typeof this.position === 'number') { - fileViewState[this.position] = editorViewState; - } + }); } - private loadTextEditorViewState(resource: URI) { - const memento = this.getMemento(this.storageService, Scope.WORKSPACE); - const editorViewStateMemento: { [key: string]: IWalkThroughEditorViewStates } = memento[WALK_THROUGH_EDITOR_VIEW_STATE_PREFERENCE_KEY]; - if (editorViewStateMemento) { - const fileViewState = editorViewStateMemento[resource.toString()]; - if (fileViewState) { - const state = fileViewState[this.position]; - if (state) { - this.scrollbar.setScrollPosition(state.viewState); - } - } + private loadTextEditorViewState(input: WalkThroughInput) { + const state = this.editorViewStateMemento.loadState(input, this.position); + if (state) { + this.scrollbar.setScrollPosition(state.viewState); } } public clearInput(): void { if (this.input instanceof WalkThroughInput) { - this.saveTextEditorViewState(this.input.getResource()); + this.saveTextEditorViewState(this.input); } super.clearInput(); } public shutdown(): void { if (this.input instanceof WalkThroughInput) { - this.saveTextEditorViewState(this.input.getResource()); + this.saveTextEditorViewState(this.input); } super.shutdown(); } + protected saveMemento(): void { + + // ensure to first save our view state memento + this.editorViewStateMemento.save(); + + super.saveMemento(); + } + dispose(): void { this.editorFocus.reset(); this.contentDisposables = dispose(this.contentDisposables); diff --git a/src/vs/workbench/services/backup/test/node/backupFileService.test.ts b/src/vs/workbench/services/backup/test/electron-browser/backupFileService.test.ts similarity index 97% rename from src/vs/workbench/services/backup/test/node/backupFileService.test.ts rename to src/vs/workbench/services/backup/test/electron-browser/backupFileService.test.ts index aa59da270d7..3af98297733 100644 --- a/src/vs/workbench/services/backup/test/node/backupFileService.test.ts +++ b/src/vs/workbench/services/backup/test/electron-browser/backupFileService.test.ts @@ -14,9 +14,10 @@ import * as path from 'path'; import * as pfs from 'vs/base/node/pfs'; import Uri from 'vs/base/common/uri'; import { BackupFileService, BackupFilesModel } from 'vs/workbench/services/backup/node/backupFileService'; -import { FileService } from 'vs/workbench/services/files/node/fileService'; +import { FileService } from 'vs/workbench/services/files/electron-browser/fileService'; import { TextModel, createTextBufferFactory } from 'vs/editor/common/model/textModel'; -import { TestContextService, TestTextResourceConfigurationService, getRandomTestPath, TestLifecycleService, TestEnvironmentService } from 'vs/workbench/test/workbenchTestServices'; +import { TestContextService, TestTextResourceConfigurationService, getRandomTestPath, TestLifecycleService, TestEnvironmentService, TestStorageService } from 'vs/workbench/test/workbenchTestServices'; +import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; import { Workspace, toWorkspaceFolders } from 'vs/platform/workspace/common/workspace'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { DefaultEndOfLine } from 'vs/editor/common/model'; @@ -38,7 +39,7 @@ const untitledBackupPath = path.join(workspaceBackupPath, 'untitled', crypto.cre class TestBackupFileService extends BackupFileService { constructor(workspace: Uri, backupHome: string, workspacesJsonPath: string) { - const fileService = new FileService(new TestContextService(new Workspace(workspace.fsPath, workspace.fsPath, toWorkspaceFolders([{ path: workspace.fsPath }]))), TestEnvironmentService, new TestTextResourceConfigurationService(), new TestConfigurationService(), new TestLifecycleService(), { disableWatcher: true }); + const fileService = new FileService(new TestContextService(new Workspace(workspace.fsPath, workspace.fsPath, toWorkspaceFolders([{ path: workspace.fsPath }]))), TestEnvironmentService, new TestTextResourceConfigurationService(), new TestConfigurationService(), new TestLifecycleService(), new TestStorageService(), new TestNotificationService(), { disableWatcher: true }); super(workspaceBackupPath, fileService); } diff --git a/src/vs/workbench/services/configuration/common/configuration.ts b/src/vs/workbench/services/configuration/common/configuration.ts index 3bdae770e7a..344bfa78905 100644 --- a/src/vs/workbench/services/configuration/common/configuration.ts +++ b/src/vs/workbench/services/configuration/common/configuration.ts @@ -13,10 +13,6 @@ export const FOLDER_SETTINGS_PATH = `${FOLDER_CONFIG_FOLDER_NAME}/${FOLDER_SETTI export const IWorkspaceConfigurationService = createDecorator('configurationService'); export interface IWorkspaceConfigurationService extends IConfigurationService { - /** - * Returns untrusted configuration keys for the current workspace. - */ - getUnsupportedWorkspaceKeys(): string[]; } export const defaultSettingsSchemaId = 'vscode://schemas/settings/default'; @@ -29,5 +25,5 @@ export const TASKS_CONFIGURATION_KEY = 'tasks'; export const LAUNCH_CONFIGURATION_KEY = 'launch'; export const WORKSPACE_STANDALONE_CONFIGURATIONS = Object.create(null); -WORKSPACE_STANDALONE_CONFIGURATIONS[TASKS_CONFIGURATION_KEY] = `${FOLDER_CONFIG_FOLDER_NAME}/tasks.json`; -WORKSPACE_STANDALONE_CONFIGURATIONS[LAUNCH_CONFIGURATION_KEY] = `${FOLDER_CONFIG_FOLDER_NAME}/launch.json`; +WORKSPACE_STANDALONE_CONFIGURATIONS[TASKS_CONFIGURATION_KEY] = `${FOLDER_CONFIG_FOLDER_NAME}/${TASKS_CONFIGURATION_KEY}.json`; +WORKSPACE_STANDALONE_CONFIGURATIONS[LAUNCH_CONFIGURATION_KEY] = `${FOLDER_CONFIG_FOLDER_NAME}/${LAUNCH_CONFIGURATION_KEY}.json`; diff --git a/src/vs/workbench/services/configuration/common/configurationExtensionPoint.ts b/src/vs/workbench/services/configuration/common/configurationExtensionPoint.ts index 8ca3d17b3da..ddc4e0db248 100644 --- a/src/vs/workbench/services/configuration/common/configurationExtensionPoint.ts +++ b/src/vs/workbench/services/configuration/common/configurationExtensionPoint.ts @@ -32,13 +32,15 @@ const configurationEntrySchema: IJSONSchema = { type: 'object', properties: { isExecutable: { - type: 'boolean' + type: 'boolean', + deprecationMessage: 'This property is deprecated. Instead use `scope` property and set it to `application` value.' }, scope: { type: 'string', - enum: ['window', 'resource'], + enum: ['application', 'window', 'resource'], default: 'window', enumDescriptions: [ + nls.localize('scope.application.description', "Application specific configuration, which can be configured only in User settings."), nls.localize('scope.window.description', "Window specific configuration, which can be configured in the User or Workspace settings."), nls.localize('scope.resource.description', "Resource specific configuration, which can be configured in the User, Workspace or Folder settings.") ], @@ -130,7 +132,17 @@ function validateProperties(configuration: IConfigurationNode, extension: IExten for (let key in properties) { const message = validateProperty(key); const propertyConfiguration = configuration.properties[key]; - propertyConfiguration.scope = propertyConfiguration.scope && propertyConfiguration.scope.toString() === 'resource' ? ConfigurationScope.RESOURCE : ConfigurationScope.WINDOW; + if (propertyConfiguration.scope) { + if (propertyConfiguration.scope.toString() === 'application') { + propertyConfiguration.scope = ConfigurationScope.APPLICATION; + } else if (propertyConfiguration.scope.toString() === 'resource') { + propertyConfiguration.scope = ConfigurationScope.RESOURCE; + } else { + propertyConfiguration.scope = ConfigurationScope.WINDOW; + } + } else { + propertyConfiguration.scope = ConfigurationScope.WINDOW; + } propertyConfiguration.notMultiRootAdopted = !(extension.description.isBuiltin || (Array.isArray(extension.description.keywords) && extension.description.keywords.indexOf('multi-root ready') !== -1)); if (message) { extension.collector.warn(message); diff --git a/src/vs/workbench/services/configuration/common/configurationModels.ts b/src/vs/workbench/services/configuration/common/configurationModels.ts index 27be593edfe..90def0b9e15 100644 --- a/src/vs/workbench/services/configuration/common/configurationModels.ts +++ b/src/vs/workbench/services/configuration/common/configurationModels.ts @@ -5,30 +5,15 @@ 'use strict'; import { equals } from 'vs/base/common/objects'; -import { compare, toValuesTree, IConfigurationChangeEvent, ConfigurationTarget, IConfigurationModel, IConfigurationOverrides, IOverrides } from 'vs/platform/configuration/common/configuration'; +import { compare, toValuesTree, IConfigurationChangeEvent, ConfigurationTarget, IConfigurationModel, IConfigurationOverrides } from 'vs/platform/configuration/common/configuration'; import { Configuration as BaseConfiguration, ConfigurationModelParser, ConfigurationChangeEvent, ConfigurationModel, AbstractConfigurationChangeEvent } from 'vs/platform/configuration/common/configurationModels'; import { Registry } from 'vs/platform/registry/common/platform'; import { IConfigurationRegistry, IConfigurationPropertySchema, Extensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; import { IStoredWorkspaceFolder } from 'vs/platform/workspaces/common/workspaces'; import { Workspace } from 'vs/platform/workspace/common/workspace'; -import { StrictResourceMap } from 'vs/base/common/map'; +import { ResourceMap } from 'vs/base/common/map'; import URI from 'vs/base/common/uri'; -export class SettingsModel extends ConfigurationModel { - - private _unsupportedKeys: string[]; - - constructor(contents: any, keys: string[], overrides: IOverrides[], unsupportedKeys: string[]) { - super(contents, keys, overrides); - this._unsupportedKeys = unsupportedKeys; - } - - public get unsupportedKeys(): string[] { - return this._unsupportedKeys; - } - -} - export class WorkspaceConfigurationModelParser extends ConfigurationModelParser { private _folders: IStoredWorkspaceFolder[] = []; @@ -37,7 +22,7 @@ export class WorkspaceConfigurationModelParser extends ConfigurationModelParser constructor(name: string) { super(name); - this._settingsModelParser = new FolderSettingsModelParser(name); + this._settingsModelParser = new FolderSettingsModelParser(name, [ConfigurationScope.WINDOW, ConfigurationScope.RESOURCE]); this._launchModel = new ConfigurationModel(); } @@ -45,8 +30,8 @@ export class WorkspaceConfigurationModelParser extends ConfigurationModelParser return this._folders; } - get settingsModel(): SettingsModel { - return this._settingsModelParser.settingsModel; + get settingsModel(): ConfigurationModel { + return this._settingsModelParser.configurationModel; } get launchModel(): ConfigurationModel { @@ -96,9 +81,9 @@ export class StandaloneConfigurationModelParser extends ConfigurationModelParser export class FolderSettingsModelParser extends ConfigurationModelParser { private _raw: any; - private _settingsModel: SettingsModel; + private _settingsModel: ConfigurationModel; - constructor(name: string, private configurationScope?: ConfigurationScope) { + constructor(name: string, private scopes: ConfigurationScope[]) { super(name); } @@ -108,11 +93,7 @@ export class FolderSettingsModelParser extends ConfigurationModelParser { } get configurationModel(): ConfigurationModel { - return this._settingsModel || new SettingsModel({}, [], [], []); - } - - get settingsModel(): SettingsModel { - return this.configurationModel; + return this._settingsModel || new ConfigurationModel(); } reprocess(): void { @@ -120,34 +101,22 @@ export class FolderSettingsModelParser extends ConfigurationModelParser { } private parseWorkspaceSettings(rawSettings: any): void { - const unsupportedKeys = []; const rawWorkspaceSettings = {}; const configurationProperties = Registry.as(Extensions.Configuration).getConfigurationProperties(); for (let key in rawSettings) { - if (this.isNotExecutable(key, configurationProperties)) { - if (this.configurationScope === void 0 || this.getScope(key, configurationProperties) === this.configurationScope) { - rawWorkspaceSettings[key] = rawSettings[key]; - } - } else { - unsupportedKeys.push(key); + const scope = this.getScope(key, configurationProperties); + if (this.scopes.indexOf(scope) !== -1) { + rawWorkspaceSettings[key] = rawSettings[key]; } } const configurationModel = this.parseRaw(rawWorkspaceSettings); - this._settingsModel = new SettingsModel(configurationModel.contents, configurationModel.keys, configurationModel.overrides, unsupportedKeys); + this._settingsModel = new ConfigurationModel(configurationModel.contents, configurationModel.keys, configurationModel.overrides); } private getScope(key: string, configurationProperties: { [qualifiedKey: string]: IConfigurationPropertySchema }): ConfigurationScope { const propertySchema = configurationProperties[key]; return propertySchema ? propertySchema.scope : ConfigurationScope.WINDOW; } - - private isNotExecutable(key: string, configurationProperties: { [qualifiedKey: string]: IConfigurationPropertySchema }): boolean { - const propertySchema = configurationProperties[key]; - if (!propertySchema) { - return true; // Unknown propertis are ignored from checks - } - return !propertySchema.isExecutable; - } } export class Configuration extends BaseConfiguration { @@ -156,9 +125,9 @@ export class Configuration extends BaseConfiguration { defaults: ConfigurationModel, user: ConfigurationModel, workspaceConfiguration: ConfigurationModel, - folders: StrictResourceMap, + folders: ResourceMap, memoryConfiguration: ConfigurationModel, - memoryConfigurationByResource: StrictResourceMap, + memoryConfigurationByResource: ResourceMap, private readonly _workspace: Workspace) { super(defaults, user, workspaceConfiguration, folders, memoryConfiguration, memoryConfigurationByResource); } @@ -266,8 +235,8 @@ export class AllKeysConfigurationChangeEvent extends AbstractConfigurationChange return this._changedConfiguration; } - get changedConfigurationByResource(): StrictResourceMap { - return new StrictResourceMap(); + get changedConfigurationByResource(): ResourceMap { + return new ResourceMap(); } get affectedKeys(): string[] { @@ -287,7 +256,7 @@ export class WorkspaceConfigurationChangeEvent implements IConfigurationChangeEv return this.configurationChangeEvent.changedConfiguration; } - get changedConfigurationByResource(): StrictResourceMap { + get changedConfigurationByResource(): ResourceMap { return this.configurationChangeEvent.changedConfigurationByResource; } @@ -317,4 +286,4 @@ export class WorkspaceConfigurationChangeEvent implements IConfigurationChangeEv return false; } -} \ 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 70082cd28ba..b763c271181 100644 --- a/src/vs/workbench/services/configuration/node/configuration.ts +++ b/src/vs/workbench/services/configuration/node/configuration.ts @@ -4,74 +4,31 @@ *--------------------------------------------------------------------------------------------*/ import URI from 'vs/base/common/uri'; +import { createHash } from 'crypto'; 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 { readFile } from 'vs/base/node/pfs'; +import * as pfs from 'vs/base/node/pfs'; import * as errors from 'vs/base/common/errors'; import * as collections from 'vs/base/common/collections'; import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle'; import { RunOnceScheduler } from 'vs/base/common/async'; -import { FileChangeType, FileChangesEvent } from 'vs/platform/files/common/files'; +import { FileChangeType, FileChangesEvent, IContent, IFileService } from 'vs/platform/files/common/files'; import { isLinux } from 'vs/base/common/platform'; import { ConfigWatcher } from 'vs/base/node/config'; -import { ConfigurationModel, ConfigurationModelParser } from 'vs/platform/configuration/common/configurationModels'; +import { ConfigurationModel } from 'vs/platform/configuration/common/configurationModels'; import { WorkspaceConfigurationModelParser, FolderSettingsModelParser, StandaloneConfigurationModelParser } from 'vs/workbench/services/configuration/common/configurationModels'; -import { WORKSPACE_STANDALONE_CONFIGURATIONS, FOLDER_SETTINGS_PATH, TASKS_CONFIGURATION_KEY, LAUNCH_CONFIGURATION_KEY } from 'vs/workbench/services/configuration/common/configuration'; +import { FOLDER_SETTINGS_PATH, TASKS_CONFIGURATION_KEY, FOLDER_SETTINGS_NAME, LAUNCH_CONFIGURATION_KEY } from 'vs/workbench/services/configuration/common/configuration'; import { IStoredWorkspace, IStoredWorkspaceFolder } from 'vs/platform/workspaces/common/workspaces'; import * as extfs from 'vs/base/node/extfs'; import { JSONEditingService } from 'vs/workbench/services/configuration/node/jsonEditingService'; -import { WorkbenchState } from 'vs/platform/workspace/common/workspace'; +import { WorkbenchState, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; import { relative } from 'path'; import { equals } from 'vs/base/common/objects'; - -// node.hs helper functions - -interface IStat { - resource: URI; - isDirectory?: boolean; - children?: { resource: URI; }[]; -} - -interface IContent { - resource: URI; - value: string; -} - -function resolveContents(resources: URI[]): TPromise { - const contents: IContent[] = []; - - return TPromise.join(resources.map(resource => { - return resolveContent(resource).then(content => { - contents.push(content); - }); - })).then(() => contents); -} - -function resolveContent(resource: URI): TPromise { - return readFile(resource.fsPath).then(contents => ({ resource, value: contents.toString() })); -} - -function resolveStat(resource: URI): TPromise { - return new TPromise((c, e) => { - extfs.readdir(resource.fsPath, (error, children) => { - if (error) { - if ((error).code === 'ENOTDIR') { - c({ resource }); - } else { - e(error); - } - } else { - c({ - resource, - isDirectory: true, - children: children.map(child => { return { resource: URI.file(paths.join(resource.fsPath, child)) }; }) - }); - } - }); - }); -} +import { Schemas } from 'vs/base/common/network'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IConfigurationModel } from 'vs/platform/configuration/common/configuration'; export class WorkspaceConfiguration extends Disposable { @@ -135,10 +92,6 @@ export class WorkspaceConfiguration extends Disposable { return this._cache; } - getUnsupportedKeys(): string[] { - return this._workspaceConfigurationModelParser.settingsModel.unsupportedKeys; - } - reprocessWorkspaceSettings(): ConfigurationModel { this._workspaceConfigurationModelParser.reprocessWorkspaceSettings(); this.consolidate(); @@ -170,108 +123,192 @@ export class WorkspaceConfiguration extends Disposable { } } -export class FolderConfiguration extends Disposable { +function isFolderConfigurationFile(resource: URI): boolean { + const name = paths.basename(resource.path); + return [`${FOLDER_SETTINGS_NAME}.json`, `${TASKS_CONFIGURATION_KEY}.json`, `${LAUNCH_CONFIGURATION_KEY}.json`].some(p => p === name);// only workspace config files +} - private static readonly RELOAD_CONFIGURATION_DELAY = 50; +export interface IFolderConfiguration { + readonly onDidChange: Event; + readonly loaded: boolean; + loadConfiguration(): TPromise; + reprocess(): ConfigurationModel; + dispose(): void; +} - private bulkFetchFromWorkspacePromise: TPromise; - private workspaceFilePathToConfiguration: { [relativeWorkspacePath: string]: TPromise }; +export abstract class AbstractFolderConfiguration extends Disposable implements IFolderConfiguration { private _folderSettingsModelParser: FolderSettingsModelParser; - private _standAloneConfigurations: ConfigurationModel[] = []; - private _cache: ConfigurationModel = new ConfigurationModel(); + private _standAloneConfigurations: ConfigurationModel[]; + private _cache: ConfigurationModel; + private _loaded: boolean = false; - private reloadConfigurationScheduler: RunOnceScheduler; - private readonly reloadConfigurationEventEmitter: Emitter = new Emitter(); + protected readonly _onDidChange: Emitter = this._register(new Emitter()); + readonly onDidChange: Event = this._onDidChange.event; - constructor(private folder: URI, private configFolderRelativePath: string, workbenchState: WorkbenchState) { + constructor(protected readonly folder: URI, workbenchState: WorkbenchState, from?: AbstractFolderConfiguration) { super(); - this._folderSettingsModelParser = new FolderSettingsModelParser(FOLDER_SETTINGS_PATH, WorkbenchState.WORKSPACE === workbenchState ? ConfigurationScope.RESOURCE : void 0); - this.workspaceFilePathToConfiguration = Object.create(null); - this.reloadConfigurationScheduler = this._register(new RunOnceScheduler(() => this.loadConfiguration().then(configuration => this.reloadConfigurationEventEmitter.fire(configuration), errors.onUnexpectedError), FolderConfiguration.RELOAD_CONFIGURATION_DELAY)); + this._folderSettingsModelParser = from ? from._folderSettingsModelParser : new FolderSettingsModelParser(FOLDER_SETTINGS_PATH, WorkbenchState.WORKSPACE === workbenchState ? [ConfigurationScope.RESOURCE] : [ConfigurationScope.WINDOW, ConfigurationScope.RESOURCE]); + this._standAloneConfigurations = from ? from._standAloneConfigurations : []; + this._cache = from ? from._cache : new ConfigurationModel(); + } + + get loaded(): boolean { + return this._loaded; } loadConfiguration(): TPromise { - // Load workspace locals - return this.loadWorkspaceConfigFiles().then(workspaceConfigFiles => { - this._standAloneConfigurations = Object.keys(workspaceConfigFiles).filter(key => key !== FOLDER_SETTINGS_PATH).map(key => workspaceConfigFiles[key].configurationModel); - // Consolidate (support *.json files in the workspace settings folder) - this.consolidate(); - return this._cache; - }); + return this.loadFolderConfigurationContents() + .then((contents) => { + this.parseContents(contents); + // Consolidate (support *.json files in the workspace settings folder) + this.consolidate(); + this._loaded = true; + return this._cache; + }); } reprocess(): ConfigurationModel { - const oldContents = this._folderSettingsModelParser.settingsModel.contents; + const oldContents = this._folderSettingsModelParser.configurationModel.contents; this._folderSettingsModelParser.reprocess(); - if (!equals(oldContents, this._folderSettingsModelParser.settingsModel.contents)) { + if (!equals(oldContents, this._folderSettingsModelParser.configurationModel.contents)) { this.consolidate(); } return this._cache; } - getUnsupportedKeys(): string[] { - return this._folderSettingsModelParser.settingsModel.unsupportedKeys; - } - private consolidate(): void { - this._cache = this._folderSettingsModelParser.settingsModel.merge(...this._standAloneConfigurations); + this._cache = this._folderSettingsModelParser.configurationModel.merge(...this._standAloneConfigurations); } - private loadWorkspaceConfigFiles(): TPromise<{ [relativeWorkspacePath: string]: ConfigurationModelParser }> { - // once: when invoked for the first time we fetch json files that contribute settings - if (!this.bulkFetchFromWorkspacePromise) { - this.bulkFetchFromWorkspacePromise = resolveStat(this.toResource(this.configFolderRelativePath)).then(stat => { - if (!stat.isDirectory) { - return TPromise.as([]); + private parseContents(contents: { resource: URI, value: string }[]): void { + this._standAloneConfigurations = []; + for (const content of contents) { + const name = paths.basename(content.resource.path); + if (name === `${FOLDER_SETTINGS_NAME}.json`) { + this._folderSettingsModelParser.parse(content.value); + } else { + const matches = /([^\.]*)*\.json/.exec(name); + if (matches && matches[1]) { + const standAloneConfigurationModelParser = new StandaloneConfigurationModelParser(content.resource.toString(), matches[1]); + standAloneConfigurationModelParser.parse(content.value); + this._standAloneConfigurations.push(standAloneConfigurationModelParser.configurationModel); } + } + } + } - return resolveContents(stat.children.filter(stat => { - const isJson = paths.extname(stat.resource.fsPath) === '.json'; - if (!isJson) { - return false; // only JSON files + protected abstract loadFolderConfigurationContents(): TPromise<{ resource: URI, value: string }[]>; +} + +export class NodeBasedFolderConfiguration extends AbstractFolderConfiguration { + + private readonly folderConfigurationPath: URI; + + constructor(folder: URI, configFolderRelativePath: string, workbenchState: WorkbenchState) { + super(folder, workbenchState); + this.folderConfigurationPath = URI.file(paths.join(this.folder.fsPath, configFolderRelativePath)); + } + + protected loadFolderConfigurationContents(): TPromise<{ resource: URI, value: string }[]> { + return this.resolveStat(this.folderConfigurationPath).then(stat => { + if (!stat.isDirectory) { + return TPromise.as([]); + } + return this.resolveContents(stat.children.filter(stat => isFolderConfigurationFile(stat.resource)) + .map(stat => stat.resource)); + }, err => [] /* never fail this call */) + .then(null, errors.onUnexpectedError); + } + + private resolveContents(resources: URI[]): TPromise<{ resource: URI, value: string }[]> { + return TPromise.join(resources.map(resource => + pfs.readFile(resource.fsPath) + .then(contents => ({ resource, value: contents.toString() })))); + } + + private resolveStat(resource: URI): TPromise<{ resource: URI, isDirectory?: boolean, children?: { resource: URI; }[] }> { + return new TPromise<{ resource: URI, isDirectory?: boolean, children?: { resource: URI; }[] }>((c, e) => { + extfs.readdir(resource.fsPath, (error, children) => { + if (error) { + if ((error).code === 'ENOTDIR') { + c({ resource }); + } else { + e(error); } + } else { + c({ + resource, + isDirectory: true, + children: children.map(child => { return { resource: URI.file(paths.join(resource.fsPath, child)) }; }) + }); + } + }); + }); + } +} - return this.isWorkspaceConfigurationFile(this.toFolderRelativePath(stat.resource)); // only workspace config files - }).map(stat => stat.resource)); - }, err => [] /* never fail this call */) - .then((contents: IContent[]) => { - contents.forEach(content => this.workspaceFilePathToConfiguration[this.toFolderRelativePath(content.resource)] = TPromise.as(this.createConfigurationModelParser(content))); - }, errors.onUnexpectedError); +export class FileServiceBasedFolderConfiguration extends AbstractFolderConfiguration { + + private bulkContentFetchromise: TPromise; + private workspaceFilePathToConfiguration: { [relativeWorkspacePath: string]: TPromise }; + private reloadConfigurationScheduler: RunOnceScheduler; + private readonly folderConfigurationPath: URI; + + constructor(folder: URI, private configFolderRelativePath: string, workbenchState: WorkbenchState, private fileService: IFileService, from?: AbstractFolderConfiguration) { + super(folder, workbenchState, from); + this.folderConfigurationPath = folder.with({ path: paths.join(this.folder.path, configFolderRelativePath) }); + this.workspaceFilePathToConfiguration = Object.create(null); + this.reloadConfigurationScheduler = this._register(new RunOnceScheduler(() => this._onDidChange.fire(), 50)); + this._register(fileService.onFileChanges(e => this.handleWorkspaceFileEvents(e))); + } + + protected loadFolderConfigurationContents(): TPromise<{ resource: URI, value: string }[]> { + // once: when invoked for the first time we fetch json files that contribute settings + if (!this.bulkContentFetchromise) { + this.bulkContentFetchromise = this.fileService.resolveFile(this.folderConfigurationPath) + .then(stat => { + if (stat.isDirectory && stat.children) { + stat.children + .filter(child => isFolderConfigurationFile(child.resource)) + .forEach(child => this.workspaceFilePathToConfiguration[this.toFolderRelativePath(child.resource)] = this.fileService.resolveContent(child.resource).then(null, errors.onUnexpectedError)); + } + }).then(null, err => [] /* never fail this call */); } // on change: join on *all* configuration file promises so that we can merge them into a single configuration object. this // happens whenever a config file changes, is deleted, or added - return this.bulkFetchFromWorkspacePromise.then(() => TPromise.join(this.workspaceFilePathToConfiguration)); + return this.bulkContentFetchromise.then(() => TPromise.join(this.workspaceFilePathToConfiguration).then(result => collections.values(result))); } - public handleWorkspaceFileEvents(event: FileChangesEvent): TPromise { + private handleWorkspaceFileEvents(event: FileChangesEvent): void { const events = event.changes; let affectedByChanges = false; // Find changes that affect workspace configuration files for (let i = 0, len = events.length; i < len; i++) { const resource = events[i].resource; - const isJson = paths.extname(resource.fsPath) === '.json'; - const isDeletedSettingsFolder = (events[i].type === FileChangeType.DELETED && paths.basename(resource.fsPath) === this.configFolderRelativePath); + const folderRelativePath = this.toFolderRelativePath(resource); + if (!folderRelativePath) { + continue; // event is not inside folder + } + + const basename = paths.basename(resource.path); + const isJson = paths.extname(basename) === '.json'; + const isDeletedSettingsFolder = (events[i].type === FileChangeType.DELETED && basename === this.configFolderRelativePath); if (!isJson && !isDeletedSettingsFolder) { continue; // only JSON files or the actual settings folder } - const workspacePath = this.toFolderRelativePath(resource); - if (!workspacePath) { - continue; // event is not inside workspace - } - // Handle case where ".vscode" got deleted - if (workspacePath === this.configFolderRelativePath && events[i].type === FileChangeType.DELETED) { + if (isDeletedSettingsFolder) { this.workspaceFilePathToConfiguration = Object.create(null); affectedByChanges = true; } // only valid workspace config files - if (!this.isWorkspaceConfigurationFile(workspacePath)) { + if (!isFolderConfigurationFile(resource)) { continue; } @@ -279,72 +316,176 @@ export class FolderConfiguration extends Disposable { // remove promises for delete events switch (events[i].type) { case FileChangeType.DELETED: - affectedByChanges = collections.remove(this.workspaceFilePathToConfiguration, workspacePath); + affectedByChanges = collections.remove(this.workspaceFilePathToConfiguration, folderRelativePath); break; case FileChangeType.UPDATED: case FileChangeType.ADDED: - this.workspaceFilePathToConfiguration[workspacePath] = resolveContent(resource).then(content => this.createConfigurationModelParser(content), errors.onUnexpectedError); + this.workspaceFilePathToConfiguration[folderRelativePath] = this.fileService.resolveContent(resource).then(null, errors.onUnexpectedError); affectedByChanges = true; } } - if (!affectedByChanges) { - return TPromise.as(null); + if (affectedByChanges) { + this.reloadConfigurationScheduler.schedule(); } + } - return new TPromise((c, e) => { - let disposable = this.reloadConfigurationEventEmitter.event(configuration => { - disposable.dispose(); - c(configuration); - }); - // trigger reload of the configuration if we are affected by changes - if (!this.reloadConfigurationScheduler.isScheduled()) { - this.reloadConfigurationScheduler.schedule(); + private toFolderRelativePath(resource: URI): string { + if (resource.scheme === Schemas.file) { + if (paths.isEqualOrParent(resource.fsPath, this.folder.fsPath, !isLinux /* ignorecase */)) { + return paths.normalize(relative(this.folder.fsPath, resource.fsPath)); } + } else { + if (paths.isEqualOrParent(resource.path, this.folder.path, true /* ignorecase */)) { + return paths.normalize(relative(this.folder.path, resource.path)); + } + } + return null; + } +} + +export class CachedFolderConfiguration extends Disposable implements IFolderConfiguration { + + private readonly _onDidChange: Emitter = this._register(new Emitter()); + readonly onDidChange: Event = this._onDidChange.event; + + private readonly cachedFolderPath: string; + private readonly cachedConfigurationPath: string; + private configurationModel: ConfigurationModel; + + loaded: boolean = false; + + constructor( + folder: URI, + configFolderRelativePath: string, + environmentService: IEnvironmentService) { + super(); + this.cachedFolderPath = paths.join(environmentService.appSettingsHome, createHash('md5').update(paths.join(folder.path, configFolderRelativePath)).digest('hex')); + this.cachedConfigurationPath = paths.join(this.cachedFolderPath, 'configuration.json'); + this.configurationModel = new ConfigurationModel(); + } + + loadConfiguration(): TPromise { + return pfs.readFile(this.cachedConfigurationPath) + .then(contents => { + const parsed: IConfigurationModel = JSON.parse(contents.toString()); + this.configurationModel = new ConfigurationModel(parsed.contents, parsed.keys, parsed.overrides); + this.loaded = true; + return this.configurationModel; + }, () => this.configurationModel); + } + + updateConfiguration(configurationModel: ConfigurationModel): TPromise { + const raw = JSON.stringify(configurationModel.toJSON()); + return this.createCachedFolder().then(created => { + if (created) { + return configurationModel.keys.length ? pfs.writeFile(this.cachedConfigurationPath, raw) : pfs.rimraf(this.cachedFolderPath); + } + return null; }); } - private createConfigurationModelParser(content: IContent): ConfigurationModelParser { - const path = this.toFolderRelativePath(content.resource); - if (path === FOLDER_SETTINGS_PATH) { - this._folderSettingsModelParser.parse(content.value); - return this._folderSettingsModelParser; - } else { - const matches = /\/([^\.]*)*\.json/.exec(path); - if (matches && matches[1]) { - const standAloneConfigurationModelParser = new StandaloneConfigurationModelParser(content.resource.toString(), matches[1]); - standAloneConfigurationModelParser.parse(content.value); - return standAloneConfigurationModelParser; + reprocess(): ConfigurationModel { + return this.configurationModel; + } + + getUnsupportedKeys(): string[] { + return []; + } + + private createCachedFolder(): TPromise { + return pfs.exists(this.cachedFolderPath) + .then(null, () => false) + .then(exists => exists ? exists : pfs.mkdirp(this.cachedFolderPath).then(() => true, () => false)); + } +} + +export class FolderConfiguration extends Disposable implements IFolderConfiguration { + + protected readonly _onDidChange: Emitter = this._register(new Emitter()); + readonly onDidChange: Event = this._onDidChange.event; + + private folderConfiguration: IFolderConfiguration; + private cachedFolderConfiguration: CachedFolderConfiguration; + private _loaded: boolean = false; + + constructor( + readonly workspaceFolder: IWorkspaceFolder, + private readonly configFolderRelativePath: string, + private readonly workbenchState: WorkbenchState, + private environmentService: IEnvironmentService, + fileService?: IFileService + ) { + super(); + + this.cachedFolderConfiguration = new CachedFolderConfiguration(this.workspaceFolder.uri, this.configFolderRelativePath, this.environmentService); + this.folderConfiguration = this.cachedFolderConfiguration; + if (fileService) { + this.folderConfiguration = new FileServiceBasedFolderConfiguration(this.workspaceFolder.uri, this.configFolderRelativePath, this.workbenchState, fileService); + } else if (this.workspaceFolder.uri.scheme === Schemas.file) { + this.folderConfiguration = new NodeBasedFolderConfiguration(this.workspaceFolder.uri, this.configFolderRelativePath, this.workbenchState); + } + this._register(this.folderConfiguration.onDidChange(e => this.onDidFolderConfigurationChange())); + } + + loadConfiguration(): TPromise { + return this.folderConfiguration.loadConfiguration() + .then(model => { + this._loaded = this.folderConfiguration.loaded; + return model; + }); + } + + reprocess(): ConfigurationModel { + return this.folderConfiguration.reprocess(); + } + + get loaded(): boolean { + return this._loaded; + } + + adopt(fileService: IFileService): TPromise { + if (fileService) { + if (this.folderConfiguration instanceof CachedFolderConfiguration) { + return this.adoptFromCachedConfiguration(fileService); + } + + if (this.folderConfiguration instanceof NodeBasedFolderConfiguration) { + return this.adoptFromNodeBasedConfiguration(fileService); } } - return new ConfigurationModelParser(null); + return TPromise.as(false); } - private isWorkspaceConfigurationFile(folderRelativePath: string): boolean { - return [FOLDER_SETTINGS_PATH, WORKSPACE_STANDALONE_CONFIGURATIONS[TASKS_CONFIGURATION_KEY], WORKSPACE_STANDALONE_CONFIGURATIONS[LAUNCH_CONFIGURATION_KEY]].some(p => p === folderRelativePath); + private adoptFromCachedConfiguration(fileService: IFileService): TPromise { + const folderConfiguration = new FileServiceBasedFolderConfiguration(this.workspaceFolder.uri, this.configFolderRelativePath, this.workbenchState, fileService); + return folderConfiguration.loadConfiguration() + .then(() => { + this.folderConfiguration = folderConfiguration; + this._register(this.folderConfiguration.onDidChange(e => this.onDidFolderConfigurationChange())); + this.updateCache(); + return true; + }); } - private toResource(folderRelativePath: string): URI { - if (typeof folderRelativePath === 'string') { - return URI.file(paths.join(this.folder.fsPath, folderRelativePath)); + private adoptFromNodeBasedConfiguration(fileService: IFileService): TPromise { + const oldFolderConfiguration = this.folderConfiguration; + this.folderConfiguration = new FileServiceBasedFolderConfiguration(this.workspaceFolder.uri, this.configFolderRelativePath, this.workbenchState, fileService, oldFolderConfiguration); + oldFolderConfiguration.dispose(); + this._register(this.folderConfiguration.onDidChange(e => this.onDidFolderConfigurationChange())); + return TPromise.as(false); + } + + private onDidFolderConfigurationChange(): void { + this.updateCache(); + this._onDidChange.fire(); + } + + private updateCache(): TPromise { + if (this.workspaceFolder.uri.scheme !== Schemas.file && this.folderConfiguration instanceof FileServiceBasedFolderConfiguration) { + return this.folderConfiguration.loadConfiguration() + .then(configurationModel => this.cachedFolderConfiguration.updateConfiguration(configurationModel)); } - - return null; - } - - private toFolderRelativePath(resource: URI, toOSPath?: boolean): string { - if (this.contains(resource)) { - return paths.normalize(relative(this.folder.fsPath, resource.fsPath), toOSPath); - } - - return null; - } - - private contains(resource: URI): boolean { - if (resource) { - return paths.isEqualOrParent(resource.fsPath, this.folder.fsPath, !isLinux /* ignorecase */); - } - - return false; + return TPromise.as(null); } } \ No newline at end of file diff --git a/src/vs/workbench/services/configuration/node/configurationEditingService.ts b/src/vs/workbench/services/configuration/node/configurationEditingService.ts index b242c7188c6..2e4521f6e26 100644 --- a/src/vs/workbench/services/configuration/node/configurationEditingService.ts +++ b/src/vs/workbench/services/configuration/node/configurationEditingService.ts @@ -39,6 +39,11 @@ export enum ConfigurationEditingErrorCode { */ ERROR_UNKNOWN_KEY, + /** + * Error when trying to write an application setting into workspace settings. + */ + ERROR_INVALID_WORKSPACE_CONFIGURATION_APPLICATION, + /** * Error when trying to write an invalid folder configuration key to folder settings. */ @@ -192,19 +197,19 @@ export class ConfigurationEditingService { : operation.workspaceStandAloneConfigurationKey === LAUNCH_CONFIGURATION_KEY ? nls.localize('openLaunchConfiguration', "Open Launch Configuration") : null; if (openStandAloneConfigurationActionLabel) { - this.notificationService.prompt(Severity.Error, error.message, [openStandAloneConfigurationActionLabel]) - .then(option => { - if (option === 0) { - this.openFile(operation.resource); - } - }); + this.notificationService.prompt(Severity.Error, error.message, + [{ + label: openStandAloneConfigurationActionLabel, + run: () => this.openFile(operation.resource) + }] + ); } else { - this.notificationService.prompt(Severity.Error, error.message, [nls.localize('open', "Open Settings")]) - .then(option => { - if (option === 0) { - this.openSettings(operation); - } - }); + this.notificationService.prompt(Severity.Error, error.message, + [{ + label: nls.localize('open', "Open Settings"), + run: () => this.openSettings(operation) + }] + ); } } @@ -213,30 +218,30 @@ export class ConfigurationEditingService { : operation.workspaceStandAloneConfigurationKey === LAUNCH_CONFIGURATION_KEY ? nls.localize('openLaunchConfiguration', "Open Launch Configuration") : null; if (openStandAloneConfigurationActionLabel) { - this.notificationService.prompt(Severity.Error, error.message, [nls.localize('saveAndRetry', "Save and Retry"), openStandAloneConfigurationActionLabel]) - .then(option => { - switch (option) { - case 0 /* Save & Retry */: - const key = operation.key ? `${operation.workspaceStandAloneConfigurationKey}.${operation.key}` : operation.workspaceStandAloneConfigurationKey; - this.writeConfiguration(operation.target, { key, value: operation.value }, { force: true, scopes }); - break; - case 1 /* Open Config */: - this.openFile(operation.resource); - break; + this.notificationService.prompt(Severity.Error, error.message, + [{ + label: nls.localize('saveAndRetry', "Save and Retry"), + run: () => { + const key = operation.key ? `${operation.workspaceStandAloneConfigurationKey}.${operation.key}` : operation.workspaceStandAloneConfigurationKey; + this.writeConfiguration(operation.target, { key, value: operation.value }, { force: true, scopes }); } - }); + }, + { + label: openStandAloneConfigurationActionLabel, + run: () => this.openFile(operation.resource) + }] + ); } else { - this.notificationService.prompt(Severity.Error, error.message, [nls.localize('saveAndRetry', "Save and Retry"), nls.localize('open', "Open Settings")]) - .then(option => { - switch (option) { - case 0 /* Save and Retry */: - this.writeConfiguration(operation.target, { key: operation.key, value: operation.value }, { force: true, scopes }); - break; - case 1 /* Open Settings */: - this.openSettings(operation); - break; - } - }); + this.notificationService.prompt(Severity.Error, error.message, + [{ + label: nls.localize('saveAndRetry', "Save and Retry"), + run: () => this.writeConfiguration(operation.target, { key: operation.key, value: operation.value }, { force: true, scopes }) + }, + { + label: nls.localize('open', "Open Settings"), + run: () => this.openSettings(operation) + }] + ); } } @@ -274,6 +279,7 @@ export class ConfigurationEditingService { // API constraints case ConfigurationEditingErrorCode.ERROR_UNKNOWN_KEY: return nls.localize('errorUnknownKey', "Unable to write to {0} because {1} is not a registered configuration.", this.stringifyTarget(target), operation.key); + case ConfigurationEditingErrorCode.ERROR_INVALID_WORKSPACE_CONFIGURATION_APPLICATION: return nls.localize('errorInvalidWorkspaceConfigurationApplication', "Unable to write {0} to Workspace Settings. This setting can be written only into User settings.", operation.key); case ConfigurationEditingErrorCode.ERROR_INVALID_FOLDER_CONFIGURATION: return nls.localize('errorInvalidFolderConfiguration', "Unable to write to Folder Settings because {0} does not support the folder resource scope.", operation.key); case ConfigurationEditingErrorCode.ERROR_INVALID_USER_TARGET: return nls.localize('errorInvalidUserTarget', "Unable to write to User Settings because {0} does not support for global scope.", operation.key); case ConfigurationEditingErrorCode.ERROR_INVALID_WORKSPACE_TARGET: return nls.localize('errorInvalidWorkspaceTarget', "Unable to write to Workspace Settings because {0} does not support for workspace scope in a multi folder workspace.", operation.key); @@ -396,6 +402,15 @@ export class ConfigurationEditingService { return this.wrapError(ConfigurationEditingErrorCode.ERROR_NO_WORKSPACE_OPENED, target, operation); } + if (target === ConfigurationTarget.WORKSPACE) { + if (!operation.workspaceStandAloneConfigurationKey) { + const configurationProperties = Registry.as(ConfigurationExtensions.Configuration).getConfigurationProperties(); + if (configurationProperties[operation.key].scope === ConfigurationScope.APPLICATION) { + return this.wrapError(ConfigurationEditingErrorCode.ERROR_INVALID_WORKSPACE_CONFIGURATION_APPLICATION, target, operation); + } + } + } + if (target === ConfigurationTarget.WORKSPACE_FOLDER) { if (!operation.resource) { return this.wrapError(ConfigurationEditingErrorCode.ERROR_INVALID_FOLDER_TARGET, target, operation); diff --git a/src/vs/workbench/services/configuration/node/configurationService.ts b/src/vs/workbench/services/configuration/node/configurationService.ts index 72329335ed4..f304dc4c546 100644 --- a/src/vs/workbench/services/configuration/node/configurationService.ts +++ b/src/vs/workbench/services/configuration/node/configurationService.ts @@ -9,14 +9,14 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { dirname, basename } from 'path'; import * as assert from 'vs/base/common/assert'; import { Event, Emitter } from 'vs/base/common/event'; -import { StrictResourceMap } from 'vs/base/common/map'; -import { equals } from 'vs/base/common/objects'; +import { ResourceMap } from 'vs/base/common/map'; +import { equals, deepClone } from 'vs/base/common/objects'; import { Disposable } from 'vs/base/common/lifecycle'; import { Queue } from 'vs/base/common/async'; import { stat, writeFile } from 'vs/base/node/pfs'; import { IJSONContributionRegistry, Extensions as JSONExtensions } from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; import { IWorkspaceContextService, Workspace, WorkbenchState, IWorkspaceFolder, toWorkspaceFolders, IWorkspaceFoldersChangeEvent, WorkspaceFolder } from 'vs/platform/workspace/common/workspace'; -import { FileChangesEvent } from 'vs/platform/files/common/files'; +import { IFileService } from 'vs/platform/files/common/files'; import { isLinux } from 'vs/base/common/platform'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { ConfigurationChangeEvent, ConfigurationModel, DefaultConfigurationModel } from 'vs/platform/configuration/common/configurationModels'; @@ -24,7 +24,7 @@ import { IConfigurationChangeEvent, ConfigurationTarget, IConfigurationOverrides import { Configuration, WorkspaceConfigurationChangeEvent, AllKeysConfigurationChangeEvent } from 'vs/workbench/services/configuration/common/configurationModels'; import { IWorkspaceConfigurationService, FOLDER_CONFIG_FOLDER_NAME, defaultSettingsSchemaId, userSettingsSchemaId, workspaceSettingsSchemaId, folderSettingsSchemaId } from 'vs/workbench/services/configuration/common/configuration'; import { Registry } from 'vs/platform/registry/common/platform'; -import { IConfigurationNode, IConfigurationRegistry, Extensions, settingsSchema, resourceSettingsSchema, IConfigurationPropertySchema } from 'vs/platform/configuration/common/configurationRegistry'; +import { IConfigurationNode, IConfigurationRegistry, Extensions, IConfigurationPropertySchema, allSettings, windowSettings, resourceSettings, applicationSettings } from 'vs/platform/configuration/common/configurationRegistry'; import { createHash } from 'crypto'; import { getWorkspaceLabel, IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier, IStoredWorkspaceFolder, isStoredWorkspaceFolder, IWorkspaceFolderCreationData } from 'vs/platform/workspaces/common/workspaces'; import { IWindowConfiguration } from 'vs/platform/windows/common/windows'; @@ -37,9 +37,10 @@ import { WorkspaceConfiguration, FolderConfiguration } from 'vs/workbench/servic import { JSONEditingService } from 'vs/workbench/services/configuration/node/jsonEditingService'; import { Schemas } from 'vs/base/common/network'; import { massageFolderPathForWorkspace } from 'vs/platform/workspaces/node/workspaces'; -import { distinct } from 'vs/base/common/arrays'; import { UserConfiguration } from 'vs/platform/configuration/node/configuration'; import { getBaseLabel } from 'vs/base/common/labels'; +import { IJSONSchema, IJSONSchemaMap } from 'vs/base/common/jsonSchema'; +import { localize } from 'vs/nls'; export class WorkspaceService extends Disposable implements IWorkspaceConfigurationService, IWorkspaceContextService { @@ -50,7 +51,7 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat private defaultConfiguration: DefaultConfigurationModel; private userConfiguration: UserConfiguration; private workspaceConfiguration: WorkspaceConfiguration; - private cachedFolderConfigs: StrictResourceMap; + private cachedFolderConfigs: ResourceMap; private workspaceEditingQueue: Queue; @@ -66,6 +67,7 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat protected readonly _onDidChangeWorkbenchState: Emitter = this._register(new Emitter()); public readonly onDidChangeWorkbenchState: Event = this._onDidChangeWorkbenchState.event; + private fileService: IFileService; private configurationEditingService: ConfigurationEditingService; private jsonEditingService: JSONEditingService; @@ -236,7 +238,9 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat // Workspace Configuration Service Impl getConfigurationData(): IConfigurationData { - return this._configuration.toData(); + const configurationData = this._configuration.toData(); + configurationData.isComplete = this.cachedFolderConfigs.values().every(c => c.loaded); + return configurationData; } getValue(): T; @@ -291,32 +295,31 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat return this._configuration.keys(); } - getUnsupportedWorkspaceKeys(): string[] { - const unsupportedWorkspaceKeys = [...this.workspaceConfiguration.getUnsupportedKeys()]; - for (const folder of this.workspace.folders) { - unsupportedWorkspaceKeys.push(...this.cachedFolderConfigs.get(folder.uri).getUnsupportedKeys()); - } - return distinct(unsupportedWorkspaceKeys); - } - initialize(arg: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IWindowConfiguration): TPromise { return this.createWorkspace(arg) .then(workspace => this.updateWorkspaceAndInitializeConfiguration(workspace)); } - setInstantiationService(instantiationService: IInstantiationService): void { - this.configurationEditingService = instantiationService.createInstance(ConfigurationEditingService); - this.jsonEditingService = instantiationService.createInstance(JSONEditingService); + acquireFileService(fileService: IFileService): void { + this.fileService = fileService; + const changedWorkspaceFolders: IWorkspaceFolder[] = []; + TPromise.join(this.cachedFolderConfigs.values() + .map(folderConfiguration => folderConfiguration.adopt(fileService) + .then(result => { + if (result) { + changedWorkspaceFolders.push(folderConfiguration.workspaceFolder); + } + }))) + .then(() => { + for (const workspaceFolder of changedWorkspaceFolders) { + this.onWorkspaceFolderConfigurationChanged(workspaceFolder); + } + }); } - handleWorkspaceFileEvents(event: FileChangesEvent): TPromise { - switch (this.getWorkbenchState()) { - case WorkbenchState.FOLDER: - return this.onSingleFolderFileChanges(event); - case WorkbenchState.WORKSPACE: - return this.onWorkspaceFileChanges(event); - } - return TPromise.as(void 0); + acquireInstantiationService(instantiationService: IInstantiationService): void { + this.configurationEditingService = instantiationService.createInstance(ConfigurationEditingService); + this.jsonEditingService = instantiationService.createInstance(JSONEditingService); } private createWorkspace(arg: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IWindowConfiguration): TPromise { @@ -438,18 +441,18 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat private loadConfiguration(): TPromise { // reset caches - this.cachedFolderConfigs = new StrictResourceMap(); + this.cachedFolderConfigs = new ResourceMap(); const folders = this.workspace.folders; return this.loadFolderConfigurations(folders) .then((folderConfigurations) => { let workspaceConfiguration = this.getWorkspaceConfigurationModel(folderConfigurations); - const folderConfigurationModels = new StrictResourceMap(); + const folderConfigurationModels = new ResourceMap(); folderConfigurations.forEach((folderConfiguration, index) => folderConfigurationModels.set(folders[index].uri, folderConfiguration)); const currentConfiguration = this._configuration; - this._configuration = new Configuration(this.defaultConfiguration, this.userConfiguration.configurationModel, workspaceConfiguration, folderConfigurationModels, new ConfigurationModel(), new StrictResourceMap(), this.getWorkbenchState() !== WorkbenchState.EMPTY ? this.workspace : null); //TODO: Sandy Avoid passing null + this._configuration = new Configuration(this.defaultConfiguration, this.userConfiguration.configurationModel, workspaceConfiguration, folderConfigurationModels, new ConfigurationModel(), new ResourceMap(), this.getWorkbenchState() !== WorkbenchState.EMPTY ? this.workspace : null); //TODO: Sandy Avoid passing null if (currentConfiguration) { const changedKeys = this._configuration.compare(currentConfiguration); @@ -489,15 +492,29 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat private registerConfigurationSchemas(): void { if (this.workspace) { const jsonRegistry = Registry.as(JSONExtensions.JSONContribution); - jsonRegistry.registerSchema(defaultSettingsSchemaId, settingsSchema); - jsonRegistry.registerSchema(userSettingsSchemaId, settingsSchema); + const convertToNotSuggestedProperties = (properties: IJSONSchemaMap, errorMessage: string): IJSONSchemaMap => { + return Object.keys(properties).reduce((result: IJSONSchemaMap, property) => { + result[property] = deepClone(properties[property]); + result[property].deprecationMessage = errorMessage; + return result; + }, {}); + }; + + const allSettingsSchema: IJSONSchema = { properties: allSettings.properties, patternProperties: allSettings.patternProperties, additionalProperties: false, errorMessage: 'Unknown configuration setting' }; + const unsupportedApplicationSettings = convertToNotSuggestedProperties(applicationSettings.properties, localize('unsupportedApplicationSetting', "This setting can be applied only in User Settings")); + const workspaceSettingsSchema: IJSONSchema = { properties: { ...unsupportedApplicationSettings, ...windowSettings.properties, ...resourceSettings.properties }, patternProperties: allSettings.patternProperties, additionalProperties: false, errorMessage: 'Unknown configuration setting' }; + + jsonRegistry.registerSchema(defaultSettingsSchemaId, allSettingsSchema); + jsonRegistry.registerSchema(userSettingsSchemaId, allSettingsSchema); if (WorkbenchState.WORKSPACE === this.getWorkbenchState()) { - jsonRegistry.registerSchema(workspaceSettingsSchemaId, settingsSchema); - jsonRegistry.registerSchema(folderSettingsSchemaId, resourceSettingsSchema); + const unsupportedWindowSettings = convertToNotSuggestedProperties(windowSettings.properties, localize('unsupportedWindowSetting', "This setting cannot be applied now. It will be applied when you open this folder directly.")); + const folderSettingsSchema: IJSONSchema = { properties: { ...unsupportedApplicationSettings, ...unsupportedWindowSettings, ...resourceSettings.properties }, patternProperties: allSettings.patternProperties, additionalProperties: false, errorMessage: 'Unknown configuration setting' }; + jsonRegistry.registerSchema(workspaceSettingsSchemaId, workspaceSettingsSchema); + jsonRegistry.registerSchema(folderSettingsSchemaId, folderSettingsSchema); } else { - jsonRegistry.registerSchema(workspaceSettingsSchemaId, settingsSchema); - jsonRegistry.registerSchema(folderSettingsSchemaId, settingsSchema); + jsonRegistry.registerSchema(workspaceSettingsSchemaId, workspaceSettingsSchema); + jsonRegistry.registerSchema(folderSettingsSchemaId, workspaceSettingsSchema); } } } @@ -526,33 +543,7 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat return TPromise.as(null); } - private onWorkspaceFileChanges(event: FileChangesEvent): TPromise { - return TPromise.join(this.workspace.folders.map(folder => - // handle file event for each folder - this.cachedFolderConfigs.get(folder.uri).handleWorkspaceFileEvents(event) - // Update folder configuration if handled - .then(folderConfiguration => folderConfiguration ? this._configuration.compareAndUpdateFolderConfiguration(folder.uri, folderConfiguration) : new ConfigurationChangeEvent())) - ).then(changeEvents => { - const consolidateChangeEvent = changeEvents.reduce((consolidated, e) => consolidated.change(e), new ConfigurationChangeEvent()); - this.triggerConfigurationChange(consolidateChangeEvent, ConfigurationTarget.WORKSPACE_FOLDER); - }); - } - - private onSingleFolderFileChanges(event: FileChangesEvent): TPromise { - const folder = this.workspace.folders[0]; - return this.cachedFolderConfigs.get(folder.uri).handleWorkspaceFileEvents(event) - .then(folderConfiguration => { - if (folderConfiguration) { - // File change handled - this._configuration.compareAndUpdateFolderConfiguration(folder.uri, folderConfiguration); - const workspaceChangedKeys = this._configuration.compareAndUpdateWorkspaceConfiguration(folderConfiguration); - this.triggerConfigurationChange(workspaceChangedKeys, ConfigurationTarget.WORKSPACE); - } - }); - } - private onWorkspaceFolderConfigurationChanged(folder: IWorkspaceFolder, key?: string): TPromise { - this.disposeFolderConfiguration(folder); return this.loadFolderConfigurations([folder]) .then(([folderConfiguration]) => { const folderChangedKeys = this._configuration.compareAndUpdateFolderConfiguration(folder.uri, folderConfiguration); @@ -571,6 +562,8 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat // Remove the configurations of deleted folders for (const key of this.cachedFolderConfigs.keys()) { if (!this.workspace.folders.filter(folder => folder.uri.toString() === key.toString())[0]) { + const folderConfiguration = this.cachedFolderConfigs.get(key); + folderConfiguration.dispose(); this.cachedFolderConfigs.delete(key); changeEvent = changeEvent.change(this._configuration.compareAndDeleteFolderConfiguration(key)); } @@ -591,8 +584,12 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat private loadFolderConfigurations(folders: IWorkspaceFolder[]): TPromise { return TPromise.join([...folders.map(folder => { - const folderConfiguration = new FolderConfiguration(folder.uri, this.workspaceSettingsRootFolder, this.getWorkbenchState()); - this.cachedFolderConfigs.set(folder.uri, this._register(folderConfiguration)); + let folderConfiguration = this.cachedFolderConfigs.get(folder.uri); + if (!folderConfiguration) { + folderConfiguration = new FolderConfiguration(folder, this.workspaceSettingsRootFolder, this.getWorkbenchState(), this.environmentService, this.fileService); + this._register(folderConfiguration.onDidChange(() => this.onWorkspaceFolderConfigurationChanged(folder))); + this.cachedFolderConfigs.set(folder.uri, this._register(folderConfiguration)); + } return folderConfiguration.loadConfiguration(); })]); } @@ -679,13 +676,6 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat return path1 === path2; } - - private disposeFolderConfiguration(folder: IWorkspaceFolder): void { - const folderConfiguration = this.cachedFolderConfigs.get(folder.uri); - if (folderConfiguration) { - folderConfiguration.dispose(); - } - } } interface IExportedConfigurationNode { diff --git a/src/vs/workbench/services/configuration/test/common/configurationModels.test.ts b/src/vs/workbench/services/configuration/test/common/configurationModels.test.ts index 2b675b8cab6..2a397b9df49 100644 --- a/src/vs/workbench/services/configuration/test/common/configurationModels.test.ts +++ b/src/vs/workbench/services/configuration/test/common/configurationModels.test.ts @@ -13,7 +13,7 @@ import URI from 'vs/base/common/uri'; import { ConfigurationChangeEvent, ConfigurationModel } from 'vs/platform/configuration/common/configurationModels'; import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; -import { StrictResourceMap } from 'vs/base/common/map'; +import { ResourceMap } from 'vs/base/common/map'; suite('FolderSettingsModelParser', () => { @@ -32,47 +32,47 @@ suite('FolderSettingsModelParser', () => { 'default': 'isSet', scope: ConfigurationScope.RESOURCE }, - 'FolderSettingsModelParser.executable': { + 'FolderSettingsModelParser.application': { 'type': 'string', 'default': 'isSet', - isExecutable: true + scope: ConfigurationScope.APPLICATION } } }); }); test('parse all folder settings', () => { - const testObject = new FolderSettingsModelParser('settings'); + const testObject = new FolderSettingsModelParser('settings', [ConfigurationScope.RESOURCE, ConfigurationScope.WINDOW]); - testObject.parse(JSON.stringify({ 'FolderSettingsModelParser.window': 'window', 'FolderSettingsModelParser.resource': 'resource', 'FolderSettingsModelParser.executable': 'executable' })); + testObject.parse(JSON.stringify({ 'FolderSettingsModelParser.window': 'window', 'FolderSettingsModelParser.resource': 'resource', 'FolderSettingsModelParser.application': 'executable' })); assert.deepEqual(testObject.configurationModel.contents, { 'FolderSettingsModelParser': { 'window': 'window', 'resource': 'resource' } }); }); test('parse resource folder settings', () => { - const testObject = new FolderSettingsModelParser('settings', ConfigurationScope.RESOURCE); + const testObject = new FolderSettingsModelParser('settings', [ConfigurationScope.RESOURCE]); - testObject.parse(JSON.stringify({ 'FolderSettingsModelParser.window': 'window', 'FolderSettingsModelParser.resource': 'resource', 'FolderSettingsModelParser.executable': 'executable' })); + testObject.parse(JSON.stringify({ 'FolderSettingsModelParser.window': 'window', 'FolderSettingsModelParser.resource': 'resource', 'FolderSettingsModelParser.application': 'executable' })); assert.deepEqual(testObject.configurationModel.contents, { 'FolderSettingsModelParser': { 'resource': 'resource' } }); }); - test('reprocess folder settings excludes executable', () => { - const testObject = new FolderSettingsModelParser('settings'); + test('reprocess folder settings excludes application setting', () => { + const testObject = new FolderSettingsModelParser('settings', [ConfigurationScope.RESOURCE, ConfigurationScope.WINDOW]); - testObject.parse(JSON.stringify({ 'FolderSettingsModelParser.resource': 'resource', 'FolderSettingsModelParser.anotherExecutable': 'executable' })); + testObject.parse(JSON.stringify({ 'FolderSettingsModelParser.resource': 'resource', 'FolderSettingsModelParser.anotherApplicationSetting': 'executable' })); - assert.deepEqual(testObject.configurationModel.contents, { 'FolderSettingsModelParser': { 'resource': 'resource', 'anotherExecutable': 'executable' } }); + assert.deepEqual(testObject.configurationModel.contents, { 'FolderSettingsModelParser': { 'resource': 'resource', 'anotherApplicationSetting': 'executable' } }); const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); configurationRegistry.registerConfiguration({ 'id': 'FolderSettingsModelParser_2', 'type': 'object', 'properties': { - 'FolderSettingsModelParser.anotherExecutable': { + 'FolderSettingsModelParser.anotherApplicationSetting': { 'type': 'string', 'default': 'isSet', - isExecutable: true + scope: ConfigurationScope.APPLICATION } } }); @@ -194,7 +194,7 @@ suite('AllKeysConfigurationChangeEvent', () => { test('changeEvent affects keys for any resource', () => { const configuraiton = new Configuration(new ConfigurationModel({}, ['window.title', 'window.zoomLevel', 'window.restoreFullscreen', 'workbench.editor.enablePreview', 'window.restoreWindows']), - new ConfigurationModel(), new ConfigurationModel(), new StrictResourceMap(), new ConfigurationModel(), new StrictResourceMap(), null); + new ConfigurationModel(), new ConfigurationModel(), new ResourceMap(), new ConfigurationModel(), new ResourceMap(), null); let testObject = new AllKeysConfigurationChangeEvent(configuraiton, ConfigurationTarget.USER, null); assert.deepEqual(testObject.affectedKeys, ['window.title', 'window.zoomLevel', 'window.restoreFullscreen', 'workbench.editor.enablePreview', 'window.restoreWindows']); @@ -234,4 +234,4 @@ suite('AllKeysConfigurationChangeEvent', () => { assert.ok(!testObject.affectsConfiguration('files')); assert.ok(!testObject.affectsConfiguration('files', URI.file('file1'))); }); -}); \ No newline at end of file +}); diff --git a/src/vs/workbench/services/configuration/test/node/configurationEditingService.test.ts b/src/vs/workbench/services/configuration/test/electron-browser/configurationEditingService.test.ts similarity index 97% rename from src/vs/workbench/services/configuration/test/node/configurationEditingService.test.ts rename to src/vs/workbench/services/configuration/test/electron-browser/configurationEditingService.test.ts index 0712f46945d..0e42aaccf0c 100644 --- a/src/vs/workbench/services/configuration/test/node/configurationEditingService.test.ts +++ b/src/vs/workbench/services/configuration/test/electron-browser/configurationEditingService.test.ts @@ -18,11 +18,12 @@ import { parseArgs } from 'vs/platform/environment/node/argv'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { EnvironmentService } from 'vs/platform/environment/node/environmentService'; import * as extfs from 'vs/base/node/extfs'; -import { TestTextFileService, TestTextResourceConfigurationService, workbenchInstantiationService, TestLifecycleService, TestEnvironmentService } from 'vs/workbench/test/workbenchTestServices'; +import { TestTextFileService, TestTextResourceConfigurationService, workbenchInstantiationService, TestLifecycleService, TestEnvironmentService, TestStorageService } from 'vs/workbench/test/workbenchTestServices'; +import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; import * as uuid from 'vs/base/common/uuid'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry'; import { WorkspaceService } from 'vs/workbench/services/configuration/node/configurationService'; -import { FileService } from 'vs/workbench/services/files/node/fileService'; +import { FileService } from 'vs/workbench/services/files/electron-browser/fileService'; import { ConfigurationEditingService, ConfigurationEditingError, ConfigurationEditingErrorCode } from 'vs/workbench/services/configuration/node/configurationEditingService'; import { IFileService } from 'vs/platform/files/common/files'; import { WORKSPACE_STANDALONE_CONFIGURATIONS } from 'vs/workbench/services/configuration/common/configuration'; @@ -104,7 +105,7 @@ suite('ConfigurationEditingService', () => { instantiationService.stub(IWorkspaceContextService, workspaceService); return workspaceService.initialize(noWorkspace ? {} as IWindowConfiguration : workspaceDir).then(() => { instantiationService.stub(IConfigurationService, workspaceService); - instantiationService.stub(IFileService, new FileService(workspaceService, TestEnvironmentService, new TestTextResourceConfigurationService(), new TestConfigurationService(), new TestLifecycleService(), { disableWatcher: true })); + instantiationService.stub(IFileService, new FileService(workspaceService, TestEnvironmentService, new TestTextResourceConfigurationService(), new TestConfigurationService(), new TestLifecycleService(), new TestStorageService(), new TestNotificationService(), { disableWatcher: true })); instantiationService.stub(ITextFileService, instantiationService.createInstance(TestTextFileService)); instantiationService.stub(ITextModelService, instantiationService.createInstance(TextModelResolverService)); instantiationService.stub(ICommandService, CommandService); diff --git a/src/vs/workbench/services/configuration/test/node/configurationService.test.ts b/src/vs/workbench/services/configuration/test/electron-browser/configurationService.test.ts similarity index 90% rename from src/vs/workbench/services/configuration/test/node/configurationService.test.ts rename to src/vs/workbench/services/configuration/test/electron-browser/configurationService.test.ts index b099e02c1b3..a04189f3079 100644 --- a/src/vs/workbench/services/configuration/test/node/configurationService.test.ts +++ b/src/vs/workbench/services/configuration/test/electron-browser/configurationService.test.ts @@ -21,11 +21,12 @@ import * as uuid from 'vs/base/common/uuid'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; import { WorkspaceService } from 'vs/workbench/services/configuration/node/configurationService'; import { ConfigurationEditingErrorCode } from 'vs/workbench/services/configuration/node/configurationEditingService'; -import { FileChangeType, FileChangesEvent, IFileService } from 'vs/platform/files/common/files'; +import { IFileService } from 'vs/platform/files/common/files'; import { IWorkspaceContextService, WorkbenchState, IWorkspaceFoldersChangeEvent } from 'vs/platform/workspace/common/workspace'; import { ConfigurationTarget, IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; -import { workbenchInstantiationService, TestTextResourceConfigurationService, TestTextFileService, TestLifecycleService, TestEnvironmentService } from 'vs/workbench/test/workbenchTestServices'; -import { FileService } from 'vs/workbench/services/files/node/fileService'; +import { workbenchInstantiationService, TestTextResourceConfigurationService, TestTextFileService, TestLifecycleService, TestEnvironmentService, TestStorageService } from 'vs/workbench/test/workbenchTestServices'; +import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; +import { FileService } from 'vs/workbench/services/files/electron-browser/fileService'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; @@ -151,10 +152,10 @@ suite('WorkspaceContextService - Workspace', () => { return workspaceService.initialize({ id: configPath, configPath }).then(() => { - instantiationService.stub(IFileService, new FileService(workspaceService, TestEnvironmentService, new TestTextResourceConfigurationService(), workspaceService, new TestLifecycleService(), { disableWatcher: true })); + instantiationService.stub(IFileService, new FileService(workspaceService, TestEnvironmentService, new TestTextResourceConfigurationService(), workspaceService, new TestLifecycleService(), new TestStorageService(), new TestNotificationService(), { disableWatcher: true })); instantiationService.stub(ITextFileService, instantiationService.createInstance(TestTextFileService)); instantiationService.stub(ITextModelService, instantiationService.createInstance(TextModelResolverService)); - workspaceService.setInstantiationService(instantiationService); + workspaceService.acquireInstantiationService(instantiationService); testObject = workspaceService; }); @@ -409,10 +410,10 @@ suite('WorkspaceService - Initialization', () => { instantiationService.stub(IEnvironmentService, environmentService); return workspaceService.initialize({}).then(() => { - instantiationService.stub(IFileService, new FileService(workspaceService, TestEnvironmentService, new TestTextResourceConfigurationService(), workspaceService, new TestLifecycleService(), { disableWatcher: true })); + instantiationService.stub(IFileService, new FileService(workspaceService, TestEnvironmentService, new TestTextResourceConfigurationService(), workspaceService, new TestLifecycleService(), new TestStorageService(), new TestNotificationService(), { disableWatcher: true })); instantiationService.stub(ITextFileService, instantiationService.createInstance(TestTextFileService)); instantiationService.stub(ITextModelService, instantiationService.createInstance(TextModelResolverService)); - workspaceService.setInstantiationService(instantiationService); + workspaceService.acquireInstantiationService(instantiationService); testObject = workspaceService; }); }); @@ -632,15 +633,15 @@ suite('WorkspaceConfigurationService - Folder', () => { 'id': '_test', 'type': 'object', 'properties': { + 'configurationService.folder.applicationSetting': { + 'type': 'string', + 'default': 'isSet', + scope: ConfigurationScope.APPLICATION + }, 'configurationService.folder.testSetting': { 'type': 'string', 'default': 'isSet', scope: ConfigurationScope.RESOURCE - }, - 'configurationService.folder.executableSetting': { - 'type': 'string', - 'default': 'isSet', - isExecutable: true } } }); @@ -662,10 +663,10 @@ suite('WorkspaceConfigurationService - Folder', () => { instantiationService.stub(IEnvironmentService, environmentService); return workspaceService.initialize(folderDir).then(() => { - instantiationService.stub(IFileService, new FileService(workspaceService, TestEnvironmentService, new TestTextResourceConfigurationService(), workspaceService, new TestLifecycleService(), { disableWatcher: true })); + instantiationService.stub(IFileService, new FileService(workspaceService, TestEnvironmentService, new TestTextResourceConfigurationService(), workspaceService, new TestLifecycleService(), new TestStorageService(), new TestNotificationService(), { disableWatcher: true })); instantiationService.stub(ITextFileService, instantiationService.createInstance(TestTextFileService)); instantiationService.stub(ITextModelService, instantiationService.createInstance(TextModelResolverService)); - workspaceService.setInstantiationService(instantiationService); + workspaceService.acquireInstantiationService(instantiationService); testObject = workspaceService; }); }); @@ -682,7 +683,7 @@ suite('WorkspaceConfigurationService - Folder', () => { }); test('defaults', () => { - assert.deepEqual(testObject.getValue('configurationService'), { 'folder': { 'testSetting': 'isSet', 'executableSetting': 'isSet' } }); + assert.deepEqual(testObject.getValue('configurationService'), { 'folder': { 'applicationSetting': 'isSet', 'testSetting': 'isSet' } }); }); test('globals override defaults', () => { @@ -731,48 +732,29 @@ suite('WorkspaceConfigurationService - Folder', () => { }); }); - test('executable settings are not read from workspace', () => { - fs.writeFileSync(globalSettingsFile, '{ "configurationService.folder.executableSetting": "userValue" }'); - fs.writeFileSync(path.join(workspaceDir, '.vscode', 'settings.json'), '{ "configurationService.folder.executableSetting": "workspaceValue" }'); + test('application settings are not read from workspace', () => { + fs.writeFileSync(globalSettingsFile, '{ "configurationService.folder.applicationSetting": "userValue" }'); + fs.writeFileSync(path.join(workspaceDir, '.vscode', 'settings.json'), '{ "configurationService.folder.applicationSetting": "workspaceValue" }'); return testObject.reloadConfiguration() - .then(() => assert.equal(testObject.getValue('configurationService.folder.executableSetting'), 'userValue')); + .then(() => assert.equal(testObject.getValue('configurationService.folder.applicationSetting'), 'userValue')); }); - test('get unsupported workspace settings', () => { - fs.writeFileSync(path.join(workspaceDir, '.vscode', 'settings.json'), '{ "configurationService.folder.executableSetting": "workspaceValue" }'); - return testObject.reloadConfiguration() - .then(() => assert.deepEqual(testObject.getUnsupportedWorkspaceKeys(), ['configurationService.folder.executableSetting'])); - }); - - test('get unsupported workspace settings after defaults are registered', () => { - fs.writeFileSync(path.join(workspaceDir, '.vscode', 'settings.json'), '{ "configurationService.folder.anotherExecutableSetting": "workspaceValue" }'); + test('get application scope settings are not loaded after defaults are registered', () => { + fs.writeFileSync(path.join(workspaceDir, '.vscode', 'settings.json'), '{ "configurationService.folder.anotherApplicationSetting": "workspaceValue" }'); return testObject.reloadConfiguration() .then(() => { configurationRegistry.registerConfiguration({ 'id': '_test', 'type': 'object', 'properties': { - 'configurationService.folder.anotherExecutableSetting': { + 'configurationService.folder.anotherApplicationSetting': { 'type': 'string', 'default': 'isSet', - isExecutable: true + scope: ConfigurationScope.APPLICATION } } }); - assert.deepEqual(testObject.getUnsupportedWorkspaceKeys(), ['configurationService.folder.anotherExecutableSetting']); - }); - }); - - test('workspace change triggers event', () => { - const settingsFile = path.join(workspaceDir, '.vscode', 'settings.json'); - fs.writeFileSync(settingsFile, '{ "configurationService.folder.testSetting": "workspaceValue" }'); - const event = new FileChangesEvent([{ resource: URI.file(settingsFile), type: FileChangeType.ADDED }]); - const target = sinon.spy(); - testObject.onDidChangeConfiguration(target); - return (testObject).handleWorkspaceFileEvents(event) - .then(() => { - assert.equal(testObject.getValue('configurationService.folder.testSetting'), 'workspaceValue'); - assert.ok(target.called); + assert.deepEqual(testObject.keys().workspace, []); }); }); @@ -880,6 +862,11 @@ suite('WorkspaceConfigurationService - Folder', () => { .then(() => assert.equal(testObject.getValue('tasks.service.testSetting'), 'value')); }); + test('update application setting into workspace configuration in a workspace is not supported', () => { + return testObject.updateValue('configurationService.folder.applicationSetting', 'workspaceValue', {}, ConfigurationTarget.WORKSPACE, true) + .then(() => assert.fail('Should not be supported'), (e) => assert.equal(e.code, ConfigurationEditingErrorCode.ERROR_INVALID_WORKSPACE_CONFIGURATION_APPLICATION)); + }); + test('update tasks configuration', () => { return testObject.updateValue('tasks', { 'version': '1.0.0', tasks: [{ 'taskName': 'myTask' }] }, ConfigurationTarget.WORKSPACE) .then(() => assert.deepEqual(testObject.getValue('tasks'), { 'version': '1.0.0', tasks: [{ 'taskName': 'myTask' }] })); @@ -922,21 +909,15 @@ suite('WorkspaceConfigurationService - Multiroot', () => { 'type': 'string', 'default': 'isSet' }, + 'configurationService.workspace.applicationSetting': { + 'type': 'string', + 'default': 'isSet', + scope: ConfigurationScope.APPLICATION + }, 'configurationService.workspace.testResourceSetting': { 'type': 'string', 'default': 'isSet', scope: ConfigurationScope.RESOURCE - }, - 'configurationService.workspace.testExecutableSetting': { - 'type': 'string', - 'default': 'isSet', - isExecutable: true - }, - 'configurationService.workspace.testExecutableResourceSetting': { - 'type': 'string', - 'default': 'isSet', - isExecutable: true, - scope: ConfigurationScope.RESOURCE } } }); @@ -958,10 +939,10 @@ suite('WorkspaceConfigurationService - Multiroot', () => { return workspaceService.initialize({ id: configPath, configPath }).then(() => { - instantiationService.stub(IFileService, new FileService(workspaceService, TestEnvironmentService, new TestTextResourceConfigurationService(), workspaceService, new TestLifecycleService(), { disableWatcher: true })); + instantiationService.stub(IFileService, new FileService(workspaceService, TestEnvironmentService, new TestTextResourceConfigurationService(), workspaceService, new TestLifecycleService(), new TestStorageService(), new TestNotificationService(), { disableWatcher: true })); instantiationService.stub(ITextFileService, instantiationService.createInstance(TestTextFileService)); instantiationService.stub(ITextModelService, instantiationService.createInstance(TextModelResolverService)); - workspaceService.setInstantiationService(instantiationService); + workspaceService.acquireInstantiationService(instantiationService); workspaceContextService = workspaceService; jsonEditingServce = instantiationService.createInstance(JSONEditingService); @@ -980,25 +961,11 @@ suite('WorkspaceConfigurationService - Multiroot', () => { return void 0; }); - test('executable settings are not read from workspace', () => { - fs.writeFileSync(environmentService.appSettingsPath, '{ "configurationService.workspace.testExecutableSetting": "userValue" }'); - return jsonEditingServce.write(workspaceContextService.getWorkspace().configuration, { key: 'settings', value: { 'configurationService.workspace.testExecutableSetting': 'workspaceValue' } }, true) + test('application settings are not read from workspace', () => { + fs.writeFileSync(environmentService.appSettingsPath, '{ "configurationService.workspace.applicationSetting": "userValue" }'); + return jsonEditingServce.write(workspaceContextService.getWorkspace().configuration, { key: 'settings', value: { 'configurationService.workspace.applicationSetting': 'workspaceValue' } }, true) .then(() => testObject.reloadConfiguration()) - .then(() => assert.equal(testObject.getValue('configurationService.workspace.testExecutableSetting'), 'userValue')); - }); - - test('executable settings are not read from workspace folder', () => { - fs.writeFileSync(environmentService.appSettingsPath, '{ "configurationService.workspace.testExecutableResourceSetting": "userValue" }'); - fs.writeFileSync(workspaceContextService.getWorkspace().folders[0].toResource('.vscode/settings.json').fsPath, '{ "configurationService.workspace.testExecutableResourceSetting": "workspaceFolderValue" }'); - return testObject.reloadConfiguration() - .then(() => assert.equal(testObject.getValue('configurationService.workspace.testExecutableResourceSetting', { resource: workspaceContextService.getWorkspace().folders[0].uri }), 'userValue')); - }); - - test('get unsupported workspace settings', () => { - fs.writeFileSync(workspaceContextService.getWorkspace().folders[0].toResource('.vscode/settings.json').fsPath, '{ "configurationService.workspace.testExecutableResourceSetting": "workspaceFolderValue" }'); - return jsonEditingServce.write(workspaceContextService.getWorkspace().configuration, { key: 'settings', value: { 'configurationService.workspace.testExecutableSetting': 'workspaceValue' } }, true) - .then(() => testObject.reloadConfiguration()) - .then(() => assert.deepEqual(testObject.getUnsupportedWorkspaceKeys(), ['configurationService.workspace.testExecutableSetting', 'configurationService.workspace.testExecutableResourceSetting'])); + .then(() => assert.equal(testObject.getValue('configurationService.workspace.applicationSetting'), 'userValue')); }); test('workspace settings override user settings after defaults are registered ', () => { @@ -1020,45 +987,30 @@ suite('WorkspaceConfigurationService - Multiroot', () => { }); }); - test('executable settings are not read from workspace folder after defaults are registered', () => { - fs.writeFileSync(environmentService.appSettingsPath, '{ "configurationService.workspace.testNewExecutableResourceSetting": "userValue" }'); - fs.writeFileSync(workspaceContextService.getWorkspace().folders[0].toResource('.vscode/settings.json').fsPath, '{ "configurationService.workspace.testNewExecutableResourceSetting": "workspaceFolderValue" }'); + test('application settings are not read from workspace folder', () => { + fs.writeFileSync(environmentService.appSettingsPath, '{ "configurationService.workspace.applicationSetting": "userValue" }'); + fs.writeFileSync(workspaceContextService.getWorkspace().folders[0].toResource('.vscode/settings.json').fsPath, '{ "configurationService.workspace.applicationSetting": "workspaceFolderValue" }'); + return testObject.reloadConfiguration() + .then(() => assert.equal(testObject.getValue('configurationService.workspace.applicationSetting'), 'userValue')); + }); + + test('application settings are not read from workspace folder after defaults are registered', () => { + fs.writeFileSync(environmentService.appSettingsPath, '{ "configurationService.workspace.testNewApplicationSetting": "userValue" }'); + fs.writeFileSync(workspaceContextService.getWorkspace().folders[0].toResource('.vscode/settings.json').fsPath, '{ "configurationService.workspace.testNewApplicationSetting": "workspaceFolderValue" }'); return testObject.reloadConfiguration() .then(() => { configurationRegistry.registerConfiguration({ 'id': '_test', 'type': 'object', 'properties': { - 'configurationService.workspace.testNewExecutableResourceSetting': { + 'configurationService.workspace.testNewApplicationSetting': { 'type': 'string', 'default': 'isSet', - isExecutable: true, - scope: ConfigurationScope.RESOURCE + scope: ConfigurationScope.APPLICATION } } }); - assert.equal(testObject.getValue('configurationService.workspace.testNewExecutableResourceSetting', { resource: workspaceContextService.getWorkspace().folders[0].uri }), 'userValue'); - }); - }); - - test('get unsupported workspace settings after defaults are registered', () => { - fs.writeFileSync(workspaceContextService.getWorkspace().folders[0].toResource('.vscode/settings.json').fsPath, '{ "configurationService.workspace.testNewExecutableResourceSetting2": "workspaceFolderValue" }'); - return jsonEditingServce.write(workspaceContextService.getWorkspace().configuration, { key: 'settings', value: { 'configurationService.workspace.testExecutableSetting': 'workspaceValue' } }, true) - .then(() => testObject.reloadConfiguration()) - .then(() => { - configurationRegistry.registerConfiguration({ - 'id': '_test', - 'type': 'object', - 'properties': { - 'configurationService.workspace.testNewExecutableResourceSetting2': { - 'type': 'string', - 'default': 'isSet', - isExecutable: true, - scope: ConfigurationScope.RESOURCE - } - } - }); - assert.deepEqual(testObject.getUnsupportedWorkspaceKeys(), ['configurationService.workspace.testExecutableSetting', 'configurationService.workspace.testNewExecutableResourceSetting2']); + assert.equal(testObject.getValue('configurationService.workspace.testNewApplicationSetting', { resource: workspaceContextService.getWorkspace().folders[0].uri }), 'userValue'); }); }); @@ -1206,6 +1158,11 @@ suite('WorkspaceConfigurationService - Multiroot', () => { .then(() => assert.ok(target.called)); }); + test('update application setting into workspace configuration in a workspace is not supported', () => { + return testObject.updateValue('configurationService.workspace.applicationSetting', 'workspaceValue', {}, ConfigurationTarget.WORKSPACE, true) + .then(() => assert.fail('Should not be supported'), (e) => assert.equal(e.code, ConfigurationEditingErrorCode.ERROR_INVALID_WORKSPACE_CONFIGURATION_APPLICATION)); + }); + test('update workspace folder configuration', () => { const workspace = workspaceContextService.getWorkspace(); return testObject.updateValue('configurationService.workspace.testResourceSetting', 'workspaceFolderValue', { resource: workspace.folders[0].uri }, ConfigurationTarget.WORKSPACE_FOLDER) diff --git a/src/vs/workbench/services/configurationResolver/electron-browser/configurationResolverService.ts b/src/vs/workbench/services/configurationResolver/electron-browser/configurationResolverService.ts index e8d49f21309..080f600ce81 100644 --- a/src/vs/workbench/services/configurationResolver/electron-browser/configurationResolverService.ts +++ b/src/vs/workbench/services/configurationResolver/electron-browser/configurationResolverService.ts @@ -3,10 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import uri from 'vs/base/common/uri'; import * as paths from 'vs/base/common/paths'; -import * as types from 'vs/base/common/types'; +import { Schemas } from 'vs/base/common/network'; import { TPromise } from 'vs/base/common/winjs.base'; import { sequence } from 'vs/base/common/async'; +import { toResource } from 'vs/workbench/common/editor'; import { IStringDictionary } from 'vs/base/common/collections'; import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; @@ -14,210 +16,11 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { ICommandService } from 'vs/platform/commands/common/commands'; import { IWorkspaceFolder, IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { toResource } from 'vs/workbench/common/editor'; -import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; +import { IProcessEnvironment } from 'vs/base/common/platform'; +import { VariableResolver } from 'vs/workbench/services/configurationResolver/node/variableResolver'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { relative } from 'path'; -import { IProcessEnvironment, isWindows } from 'vs/base/common/platform'; -import { normalizeDriveLetter } from 'vs/base/common/labels'; -import { Schemas } from 'vs/base/common/network'; -import { localize } from 'vs/nls'; +import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; -class VariableResolver { - static VARIABLE_REGEXP = /\$\{(.*?)\}/g; - private envVariables: IProcessEnvironment; - - constructor( - envVariables: IProcessEnvironment, - private configurationService: IConfigurationService, - private editorService: IWorkbenchEditorService, - private environmentService: IEnvironmentService, - private workspaceContextService: IWorkspaceContextService - ) { - if (isWindows) { - this.envVariables = Object.create(null); - Object.keys(envVariables).forEach(key => { - this.envVariables[key.toLowerCase()] = envVariables[key]; - }); - } else { - this.envVariables = envVariables; - } - } - - resolve(context: IWorkspaceFolder, value: string): string { - const filePath = this.getFilePath(); - return value.replace(VariableResolver.VARIABLE_REGEXP, (match: string, variable: string) => { - const parts = variable.split(':'); - let sufix: string; - if (parts && parts.length > 1) { - variable = parts[0]; - sufix = parts[1]; - } - - switch (variable) { - case 'env': { - if (sufix) { - if (isWindows) { - sufix = sufix.toLowerCase(); - } - - const env = this.envVariables[sufix]; - if (types.isString(env)) { - return env; - } - - // For `env` we should do the same as a normal shell does - evaluates missing envs to an empty string #46436 - return ''; - } - } - case 'config': { - if (sufix) { - const config = this.configurationService.getValue(sufix, context ? { resource: context.uri } : undefined); - if (!types.isUndefinedOrNull(config) && !types.isObject(config)) { - return config; - } - } - } - default: { - if (sufix) { - const folder = this.workspaceContextService.getWorkspace().folders.filter(f => f.name === sufix).pop(); - if (folder) { - context = folder; - } - } - - switch (variable) { - case 'workspaceRoot': - case 'workspaceFolder': { - if (context) { - return normalizeDriveLetter(context.uri.fsPath); - } - if (this.workspaceContextService.getWorkspace().folders.length > 1) { - throw new Error(localize('canNotResolveWorkspaceFolderMultiRoot', "${workspaceFolder} can not be resolved in a multi folder workspace. Scope this variables using : and a folder name.")); - } - - throw new Error(localize('canNotResolveWorkspaceFolder', "${workspaceFolder} can not be resolved. Please open a folder.")); - } case 'cwd': - return context ? normalizeDriveLetter(context.uri.fsPath) : process.cwd(); - case 'workspaceRootFolderName': - case 'workspaceFolderBasename': { - if (context) { - return paths.basename(context.uri.fsPath); - } - if (this.workspaceContextService.getWorkspace().folders.length > 1) { - throw new Error(localize('canNotResolveFolderBasenameMultiRoot', "${workspaceFolderBasename} can not be resolved in a multi folder workspace. Scope this variables using : and a folder name.")); - } - - throw new Error(localize('canNotResolveFolderBasename', "${workspaceFolderBasename} can not be resolved. Please open a folder.")); - } case 'lineNumber': { - const lineNumber = this.getLineNumber(); - if (lineNumber) { - return lineNumber; - } - - throw new Error(localize('canNotResolveLineNumber', "${lineNumber} can not be resolved, please open an editor.")); - } case 'selectedText': { - const selectedText = this.getSelectedText(); - if (selectedText) { - return selectedText; - } - - throw new Error(localize('canNotResolveSelectedText', "${selectedText} can not be resolved, please open an editor.")); - } case 'file': { - if (filePath) { - return filePath; - } - - throw new Error(localize('canNotResolveFile', "${file} can not be resolved, please open an editor.")); - } case 'relativeFile': { - if (context && filePath) { - return paths.normalize(relative(context.uri.fsPath, filePath)); - } - if (filePath) { - return filePath; - } - - throw new Error(localize('canNotResolveRelativeFile', "${relativeFile} can not be resolved, please open an editor.")); - } case 'fileDirname': { - if (filePath) { - return paths.dirname(filePath); - } - - throw new Error(localize('canNotResolveFileDirname', "${fileDirname} can not be resolved, please open an editor.")); - } case 'fileExtname': { - if (filePath) { - return paths.extname(filePath); - } - - throw new Error(localize('canNotResolveFileExtname', "${fileExtname} can not be resolved, please open an editor.")); - } case 'fileBasename': { - if (filePath) { - return paths.basename(filePath); - } - - throw new Error(localize('canNotResolveFileBasename', "${fileBasename} can not be resolved, please open an editor.")); - } case 'fileBasenameNoExtension': { - if (filePath) { - const basename = paths.basename(filePath); - return basename.slice(0, basename.length - paths.extname(basename).length); - } - - throw new Error(localize('canNotResolveFileBasenameNoExtension', "${fileBasenameNoExtension} can not be resolved, please open an editor.")); - } - case 'execPath': - return this.environmentService.execPath; - - default: - return match; - } - } - } - }); - } - - private getSelectedText(): string { - const activeEditor = this.editorService.getActiveEditor(); - if (activeEditor) { - const editorControl = (activeEditor.getControl()); - if (editorControl) { - const editorModel = editorControl.getModel(); - const editorSelection = editorControl.getSelection(); - if (editorModel && editorSelection) { - return editorModel.getValueInRange(editorSelection); - } - } - } - - return undefined; - } - - private getFilePath(): string { - let input = this.editorService.getActiveEditorInput(); - if (input instanceof DiffEditorInput) { - input = input.modifiedInput; - } - - const fileResource = toResource(input, { filter: Schemas.file }); - if (!fileResource) { - return undefined; - } - - return paths.normalize(fileResource.fsPath, true); - } - - private getLineNumber(): string { - const activeEditor = this.editorService.getActiveEditor(); - if (activeEditor) { - const editorControl = (activeEditor.getControl()); - if (editorControl) { - const lineNumber = editorControl.getSelection().positionLineNumber; - return String(lineNumber); - } - } - - return undefined; - } -} export class ConfigurationResolverService implements IConfigurationResolverService { _serviceBrand: any; @@ -231,42 +34,68 @@ export class ConfigurationResolverService implements IConfigurationResolverServi @ICommandService private commandService: ICommandService, @IWorkspaceContextService workspaceContextService: IWorkspaceContextService ) { - this.resolver = new VariableResolver(envVariables, configurationService, editorService, environmentService, workspaceContextService); + this.resolver = new VariableResolver({ + getFolderUri: (folderName: string): uri => { + const folder = workspaceContextService.getWorkspace().folders.filter(f => f.name === folderName).pop(); + return folder ? folder.uri : undefined; + }, + getWorkspaceFolderCount: (): number => { + return workspaceContextService.getWorkspace().folders.length; + }, + getConfigurationValue: (folderUri: uri, suffix: string) => { + return configurationService.getValue(suffix, folderUri ? { resource: folderUri } : undefined); + }, + getExecPath: () => { + return environmentService['execPath']; + }, + getFilePath: (): string | undefined => { + let input = editorService.getActiveEditorInput(); + if (input instanceof DiffEditorInput) { + input = input.modifiedInput; + } + const fileResource = toResource(input, { filter: Schemas.file }); + if (!fileResource) { + return undefined; + } + return paths.normalize(fileResource.fsPath, true); + }, + getSelectedText: (): string | undefined => { + const activeEditor = editorService.getActiveEditor(); + if (activeEditor) { + const editorControl = (activeEditor.getControl()); + if (editorControl) { + const editorModel = editorControl.getModel(); + const editorSelection = editorControl.getSelection(); + if (editorModel && editorSelection) { + return editorModel.getValueInRange(editorSelection); + } + } + } + return undefined; + }, + getLineNumber: (): string => { + const activeEditor = editorService.getActiveEditor(); + if (activeEditor) { + const editorControl = (activeEditor.getControl()); + if (editorControl) { + const lineNumber = editorControl.getSelection().positionLineNumber; + return String(lineNumber); + } + } + return undefined; + } + }, envVariables); } public resolve(root: IWorkspaceFolder, value: string): string; public resolve(root: IWorkspaceFolder, value: string[]): string[]; public resolve(root: IWorkspaceFolder, value: IStringDictionary): IStringDictionary; public resolve(root: IWorkspaceFolder, value: any): any { - if (types.isString(value)) { - return this.resolver.resolve(root, value); - } else if (types.isArray(value)) { - return value.map(s => this.resolver.resolve(root, s)); - } else if (types.isObject(value)) { - let result: IStringDictionary | string[]> = Object.create(null); - Object.keys(value).forEach(key => { - result[key] = this.resolve(root, value[key]); - }); - - return result; - } - return value; + return this.resolver.resolveAny(root ? root.uri : undefined, value); } public resolveAny(root: IWorkspaceFolder, value: any): any { - if (types.isString(value)) { - return this.resolver.resolve(root, value); - } else if (types.isArray(value)) { - return value.map(s => this.resolveAny(root, s)); - } else if (types.isObject(value)) { - let result: IStringDictionary | string[]> = Object.create(null); - Object.keys(value).forEach(key => { - result[key] = this.resolveAny(root, value[key]); - }); - - return result; - } - return value; + return this.resolver.resolveAny(root ? root.uri : undefined, value); } /** diff --git a/src/vs/workbench/services/configurationResolver/node/variableResolver.ts b/src/vs/workbench/services/configurationResolver/node/variableResolver.ts new file mode 100644 index 00000000000..a8fc1e8b982 --- /dev/null +++ b/src/vs/workbench/services/configurationResolver/node/variableResolver.ts @@ -0,0 +1,209 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as paths from 'vs/base/common/paths'; +import * as types from 'vs/base/common/types'; +import { IStringDictionary } from 'vs/base/common/collections'; +import { relative } from 'path'; +import { IProcessEnvironment, isWindows } from 'vs/base/common/platform'; +import { normalizeDriveLetter } from 'vs/base/common/labels'; +import { localize } from 'vs/nls'; +import uri from 'vs/base/common/uri'; + + +export interface IVariableAccessor { + getFolderUri(folderName: string): uri | undefined; + getWorkspaceFolderCount(): number; + getConfigurationValue(folderUri: uri, section: string): string | undefined; + getExecPath(): string | undefined; + getFilePath(): string | undefined; + getSelectedText(): string | undefined; + getLineNumber(): string; +} + +export class VariableResolver { + + static VARIABLE_REGEXP = /\$\{(.*?)\}/g; + + private envVariables: IProcessEnvironment; + + constructor( + private accessor: IVariableAccessor, + envVariables: IProcessEnvironment + ) { + if (isWindows) { + this.envVariables = Object.create(null); + Object.keys(envVariables).forEach(key => { + this.envVariables[key.toLowerCase()] = envVariables[key]; + }); + } else { + this.envVariables = envVariables; + } + } + + resolveAny(folderUri: uri, value: any): any { + if (types.isString(value)) { + return this.resolve(folderUri, value); + } else if (types.isArray(value)) { + return value.map(s => this.resolveAny(folderUri, s)); + } else if (types.isObject(value)) { + let result: IStringDictionary | string[]> = Object.create(null); + Object.keys(value).forEach(key => { + result[key] = this.resolveAny(folderUri, value[key]); + }); + return result; + } + return value; + } + + resolve(folderUri: uri, value: string): string { + + const filePath = this.accessor.getFilePath(); + + return value.replace(VariableResolver.VARIABLE_REGEXP, (match: string, variable: string) => { + + let argument: string; + const parts = variable.split(':'); + if (parts && parts.length > 1) { + variable = parts[0]; + argument = parts[1]; + } + + switch (variable) { + + case 'env': + if (argument) { + if (isWindows) { + argument = argument.toLowerCase(); + } + const env = this.envVariables[argument]; + if (types.isString(env)) { + return env; + } + // For `env` we should do the same as a normal shell does - evaluates missing envs to an empty string #46436 + return ''; + } + throw new Error(localize('missingEnvVarName', "'{0}' can not be resolved because no environment variable name is given.", match)); + + case 'config': + if (argument) { + const config = this.accessor.getConfigurationValue(folderUri, argument); + if (types.isUndefinedOrNull(config)) { + throw new Error(localize('configNotFound', "'{0}' can not be resolved because setting '{1}' not found.", match, argument)); + } + if (types.isObject(config)) { + throw new Error(localize('configNoString', "'{0}' can not be resolved because '{1}' is a structured value.", match, argument)); + } + return config; + } + throw new Error(localize('missingConfigName', "'{0}' can not be resolved because no settings name is given.", match)); + + default: { + + // common error handling for all variables that require an open folder and accept a folder name argument + switch (variable) { + case 'workspaceRoot': + case 'workspaceFolder': + case 'workspaceRootFolderName': + case 'workspaceFolderBasename': + case 'relativeFile': + if (argument) { + const folder = this.accessor.getFolderUri(argument); + if (folder) { + folderUri = folder; + } else { + throw new Error(localize('canNotFindFolder', "'{0}' can not be resolved. No such folder '{1}'.", match, argument)); + } + } + if (!folderUri) { + if (this.accessor.getWorkspaceFolderCount() > 1) { + throw new Error(localize('canNotResolveWorkspaceFolderMultiRoot', "'{0}' can not be resolved in a multi folder workspace. Scope this variable using ':' and a workspace folder name.", match)); + } + throw new Error(localize('canNotResolveWorkspaceFolder', "'{0}' can not be resolved. Please open a folder.", match)); + } + break; + default: + break; + } + + // common error handling for all variables that require an open file + switch (variable) { + case 'file': + case 'relativeFile': + case 'fileDirname': + case 'fileExtname': + case 'fileBasename': + case 'fileBasenameNoExtension': + if (!filePath) { + throw new Error(localize('canNotResolveFile', "'{0}' can not be resolved. Please open an editor.", match)); + } + break; + default: + break; + } + + switch (variable) { + case 'workspaceRoot': + case 'workspaceFolder': + return normalizeDriveLetter(folderUri.fsPath); + + case 'cwd': + return folderUri ? normalizeDriveLetter(folderUri.fsPath) : process.cwd(); + + case 'workspaceRootFolderName': + case 'workspaceFolderBasename': + return paths.basename(folderUri.fsPath); + + case 'lineNumber': + const lineNumber = this.accessor.getLineNumber(); + if (lineNumber) { + return lineNumber; + } + throw new Error(localize('canNotResolveLineNumber', "'{0}' can not be resolved. Make sure to have a line selected in the active editor.", match)); + + case 'selectedText': + const selectedText = this.accessor.getSelectedText(); + if (selectedText) { + return selectedText; + } + throw new Error(localize('canNotResolveSelectedText', "'{0}' can not be resolved. Make sure to have some text selected in the active editor.", match)); + + case 'file': + return filePath; + + case 'relativeFile': + if (folderUri) { + return paths.normalize(relative(folderUri.fsPath, filePath)); + } + return filePath; + + case 'fileDirname': + return paths.dirname(filePath); + + case 'fileExtname': + return paths.extname(filePath); + + case 'fileBasename': + return paths.basename(filePath); + + case 'fileBasenameNoExtension': + const basename = paths.basename(filePath); + return basename.slice(0, basename.length - paths.extname(basename).length); + + case 'execPath': + const ep = this.accessor.getExecPath(); + if (ep) { + return ep; + } + throw new Error(localize('canNotResolveExecPath', "'{0}' can not be resolved.", match)); + + default: + return match; + } + } + } + }); + } +} diff --git a/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts b/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts index ddbc43f5c6d..5b514fb535b 100644 --- a/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts +++ b/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts @@ -86,7 +86,7 @@ suite('Configuration Resolver Service', () => { if (platform.isWindows) { assert.strictEqual(configurationResolverService.resolve(workspace, '${env:key1} - ${env:Key1}'), 'Value for key1 - Value for key1'); } else { - assert.strictEqual(configurationResolverService.resolve(workspace, '${env:key1} - ${env:Key1}'), 'Value for key1 - ${env:Key1}'); + assert.strictEqual(configurationResolverService.resolve(workspace, '${env:key1} - ${env:Key1}'), 'Value for key1 - '); } }); @@ -196,18 +196,6 @@ suite('Configuration Resolver Service', () => { assert.strictEqual(service.resolve(workspace, 'abc ${config:editor.fontFamily} ${config:editor.lineNumbers} ${config:editor.insertSpaces} xyz'), 'abc foo 123 false xyz'); }); - test('configuration should not evaluate Javascript', () => { - let configurationService: IConfigurationService; - configurationService = new MockConfigurationService({ - editor: { - abc: 'foo' - } - }); - - let service = new ConfigurationResolverService(envVariables, new TestEditorService(), TestEnvironmentService, configurationService, mockCommandService, new TestContextService()); - assert.strictEqual(service.resolve(workspace, 'abc ${config:editor[\'abc\'.substr(0)]} xyz'), 'abc ${config:editor[\'abc\'.substr(0)]} xyz'); - }); - test('uses original variable as fallback', () => { let configurationService: IConfigurationService; configurationService = new MockConfigurationService({ @@ -215,10 +203,8 @@ suite('Configuration Resolver Service', () => { }); let service = new ConfigurationResolverService(envVariables, new TestEditorService(), TestEnvironmentService, configurationService, mockCommandService, new TestContextService()); - assert.strictEqual(service.resolve(workspace, 'abc ${invalidVariable} xyz'), 'abc ${invalidVariable} xyz'); - assert.strictEqual(service.resolve(workspace, 'abc ${env:invalidVariable} xyz'), 'abc ${env:invalidVariable} xyz'); - assert.strictEqual(service.resolve(workspace, 'abc ${config:editor.abc.def} xyz'), 'abc ${config:editor.abc.def} xyz'); - assert.strictEqual(service.resolve(workspace, 'abc ${config:panel.abc} xyz'), 'abc ${config:panel.abc} xyz'); + assert.strictEqual(service.resolve(workspace, 'abc ${unknownVariable} xyz'), 'abc ${unknownVariable} xyz'); + assert.strictEqual(service.resolve(workspace, 'abc ${env:unknownVariable} xyz'), 'abc xyz'); }); test('configuration variables with invalid accessor', () => { @@ -230,9 +216,14 @@ suite('Configuration Resolver Service', () => { }); let service = new ConfigurationResolverService(envVariables, new TestEditorService(), TestEnvironmentService, configurationService, mockCommandService, new TestContextService()); - assert.strictEqual(service.resolve(workspace, 'abc ${config:} xyz'), 'abc ${config:} xyz'); - assert.strictEqual(service.resolve(workspace, 'abc ${config:editor..fontFamily} xyz'), 'abc ${config:editor..fontFamily} xyz'); - assert.strictEqual(service.resolve(workspace, 'abc ${config:editor.none.none2} xyz'), 'abc ${config:editor.none.none2} xyz'); + + assert.throws(() => service.resolve(workspace, 'abc ${env} xyz')); + assert.throws(() => service.resolve(workspace, 'abc ${env:} xyz')); + assert.throws(() => service.resolve(workspace, 'abc ${config} xyz')); + assert.throws(() => service.resolve(workspace, 'abc ${config:} xyz')); + assert.throws(() => service.resolve(workspace, 'abc ${config:editor} xyz')); + assert.throws(() => service.resolve(workspace, 'abc ${config:editor..fontFamily} xyz')); + assert.throws(() => service.resolve(workspace, 'abc ${config:editor.none.none2} xyz')); }); test('interactive variable simple', () => { diff --git a/src/vs/workbench/services/crashReporter/electron-browser/crashReporterService.ts b/src/vs/workbench/services/crashReporter/electron-browser/crashReporterService.ts index 4778b3aa678..627c221fa6c 100644 --- a/src/vs/workbench/services/crashReporter/electron-browser/crashReporterService.ts +++ b/src/vs/workbench/services/crashReporter/electron-browser/crashReporterService.ts @@ -119,7 +119,7 @@ export class CrashReporterService implements ICrashReporterService { // Experimental crash reporting support for child processes on Mac only for now if (this.isEnabled && isMacintosh) { const childProcessOptions = deepClone(this.options); - childProcessOptions.extra.processName = name; + (childProcessOptions.extra).processName = name; childProcessOptions.crashesDirectory = os.tmpdir(); return childProcessOptions; diff --git a/src/vs/workbench/services/decorations/browser/decorationsService.ts b/src/vs/workbench/services/decorations/browser/decorationsService.ts index 4f55f3044ab..dfce99a732f 100644 --- a/src/vs/workbench/services/decorations/browser/decorationsService.ts +++ b/src/vs/workbench/services/decorations/browser/decorationsService.ts @@ -245,6 +245,9 @@ class DecorationProviderWrapper { } else { // selective changes -> drop for resource, fetch again, send event + // perf: the map stores thenables, decorations, or `null`-markers. + // we make us of that and ignore all uris in which we have never + // been interested. for (const uri of uris) { this._fetchData(uri); } @@ -363,6 +366,8 @@ export class FileDecorationsService implements IDecorationsService { dispose(): void { dispose(this._disposables); + dispose(this._onDidChangeDecorations); + dispose(this._onDidChangeDecorationsDelayed); } registerDecorationsProvider(provider: IDecorationsProvider): IDisposable { diff --git a/src/vs/workbench/services/decorations/test/browser/decorationsService.test.ts b/src/vs/workbench/services/decorations/test/browser/decorationsService.test.ts index b8c9e471fab..631cc2df561 100644 --- a/src/vs/workbench/services/decorations/test/browser/decorationsService.test.ts +++ b/src/vs/workbench/services/decorations/test/browser/decorationsService.test.ts @@ -75,7 +75,7 @@ suite('DecorationsService', function () { assert.equal(callCounter, 1); }); - test('Clear decorations on provider dispose', function () { + test('Clear decorations on provider dispose', async function () { let uri = URI.parse('foo:bar'); let callCounter = 0; @@ -94,13 +94,14 @@ suite('DecorationsService', function () { // un-register -> ensure good event let didSeeEvent = false; - service.onDidChangeDecorations(e => { + let p = toPromise(service.onDidChangeDecorations).then(e => { assert.equal(e.affectsResource(uri), true); assert.deepEqual(service.getDecoration(uri, false), undefined); assert.equal(callCounter, 1); didSeeEvent = true; }); reg.dispose(); + await p; assert.equal(didSeeEvent, true); }); diff --git a/src/vs/workbench/services/extensions/common/extensionsRegistry.ts b/src/vs/workbench/services/extensions/common/extensionsRegistry.ts index 2ec528fcec0..95a5a332c4d 100644 --- a/src/vs/workbench/services/extensions/common/extensionsRegistry.ts +++ b/src/vs/workbench/services/extensions/common/extensionsRegistry.ts @@ -125,12 +125,12 @@ const schema: IJSONSchema = { properties: { engines: { type: 'object', - + description: nls.localize('vscode.extension.engines', "Engine compatibility."), properties: { 'vscode': { type: 'string', description: nls.localize('vscode.extension.engines.vscode', 'For VS Code extensions, specifies the VS Code version that the extension is compatible with. Cannot be *. For example: ^0.10.5 indicates compatibility with a minimum VS Code version of 0.10.5.'), - default: '^0.10.0', + default: '^1.22.0', } } }, @@ -147,8 +147,15 @@ const schema: IJSONSchema = { type: 'array', uniqueItems: true, items: { - type: 'string', - enum: ['Languages', 'Snippets', 'Linters', 'Themes', 'Debuggers', 'Other', 'Keymaps', 'Formatters', 'Extension Packs', 'SCM Providers', 'Azure', 'Language Packs'] + oneOf: [{ + type: 'string', + enum: ['Programming Languages', 'Snippets', 'Linters', 'Themes', 'Debuggers', 'Other', 'Keymaps', 'Formatters', 'Extension Packs', 'SCM Providers', 'Azure', 'Language Packs'], + }, + { + type: 'string', + const: 'Languages', + deprecationMessage: nls.localize('vscode.extension.category.languages.deprecated', 'Use \'Programming Languages\' instead'), + }] } }, galleryBanner: { @@ -249,6 +256,25 @@ const schema: IJSONSchema = { } } }, + markdown: { + type: 'string', + description: nls.localize('vscode.extension.markdown', "Controls the Markdown rendering engine used in the Marketplace. Either github (default) or standard."), + enum: ['github', 'standard'], + default: 'github' + }, + qna: { + default: 'marketplace', + description: nls.localize('vscode.extension.qna', "Controls the Q&A link in the Marketplace. Set to marketplace to enable the default Marketplace Q & A site. Set to a string to provide the URL of a custom Q & A site. Set to false to disable Q & A altogether."), + anyOf: [ + { + type: ['string', 'boolean'], + enum: ['marketplace', false] + }, + { + type: 'string' + } + ] + }, extensionDependencies: { description: nls.localize('vscode.extension.extensionDependencies', 'Dependencies to other extensions. The identifier of an extension is always ${publisher}.${name}. For example: vscode.csharp.'), type: 'array', diff --git a/src/vs/workbench/services/extensions/electron-browser/extensionHost.ts b/src/vs/workbench/services/extensions/electron-browser/extensionHost.ts index f6caeb161b4..7a1508c2eec 100644 --- a/src/vs/workbench/services/extensions/electron-browser/extensionHost.ts +++ b/src/vs/workbench/services/extensions/electron-browser/extensionHost.ts @@ -235,11 +235,12 @@ export class ExtensionHostProcessWorker { ? nls.localize('extensionHostProcess.startupFailDebug', "Extension host did not start in 10 seconds, it might be stopped on the first line and needs a debugger to continue.") : nls.localize('extensionHostProcess.startupFail', "Extension host did not start in 10 seconds, that might be a problem."); - this._notificationService.prompt(Severity.Warning, msg, [nls.localize('reloadWindow', "Reload Window")]).then(choice => { - if (choice === 0) { - this._windowService.reloadWindow(); - } - }); + this._notificationService.prompt(Severity.Warning, msg, + [{ + label: nls.localize('reloadWindow', "Reload Window"), + run: () => this._windowService.reloadWindow() + }] + ); }, 10000); } @@ -370,10 +371,7 @@ export class ExtensionHostProcessWorker { appSettingsHome: this._environmentService.appSettingsHome, disableExtensions: this._environmentService.disableExtensions, extensionDevelopmentPath: this._environmentService.extensionDevelopmentPath, - extensionTestsPath: this._environmentService.extensionTestsPath, - // globally disable proposed api when built and not insiders developing extensions - enableProposedApiForAll: !this._environmentService.isBuilt || (!!this._environmentService.extensionDevelopmentPath && product.nameLong.indexOf('Insiders') >= 0), - enableProposedApiFor: this._environmentService.args['enable-proposed-api'] || [] + extensionTestsPath: this._environmentService.extensionTestsPath }, workspace: this._contextService.getWorkbenchState() === WorkbenchState.EMPTY ? null : this._contextService.getWorkspace(), extensions: extensionDescriptions, diff --git a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts b/src/vs/workbench/services/extensions/electron-browser/extensionService.ts index 0423ebbecc4..7fb1c10edd1 100644 --- a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts +++ b/src/vs/workbench/services/extensions/electron-browser/extensionService.ts @@ -41,6 +41,7 @@ import product from 'vs/platform/node/product'; import * as strings from 'vs/base/common/strings'; import { RPCProtocol } from 'vs/workbench/services/extensions/node/rpcProtocol'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; +import { isFalsyOrEmpty } from 'vs/base/common/arrays'; let _SystemExtensionsRoot: string = null; function getSystemExtensionsRoot(): string { @@ -299,6 +300,10 @@ export class ExtensionService extends Disposable implements IExtensionService { this._extensionHostProcessManager = null; this.startDelayed(lifecycleService); + + if (this._environmentService.disableExtensions) { + this._notificationService.info(nls.localize('extensionsDisabled', "All extensions are disabled.")); + } } private startDelayed(lifecycleService: ILifecycleService): void { @@ -382,16 +387,16 @@ export class ExtensionService extends Disposable implements IExtensionService { message = nls.localize('extensionHostProcess.unresponsiveCrash', "Extension host terminated because it was not responsive."); } - this._notificationService.prompt(Severity.Error, message, [nls.localize('devTools', "Developer Tools"), nls.localize('restart', "Restart Extension Host")]).then(choice => { - switch (choice) { - case 0 /* Open Dev Tools */: - this._windowService.openDevTools(); - break; - case 1 /* Restart Extension Host */: - this._startExtensionHostProcess(Object.keys(this._allRequestedActivateEvents)); - break; - } - }); + this._notificationService.prompt(Severity.Error, message, + [{ + label: nls.localize('devTools', "Open Developer Tools"), + run: () => this._windowService.openDevTools() + }, + { + label: nls.localize('restart', "Restart Extension Host"), + run: () => this._startExtensionHostProcess(Object.keys(this._allRequestedActivateEvents)) + }] + ); } // ---- begin IExtensionService @@ -420,7 +425,10 @@ export class ExtensionService extends Disposable implements IExtensionService { } private _activateByEvent(activationEvent: string): TPromise { - return this._extensionHostProcessManager.activateByEvent(activationEvent); + if (this._extensionHostProcessManager) { + return this._extensionHostProcessManager.activateByEvent(activationEvent); + } + return NO_OP_VOID_PROMISE; } public whenInstalledExtensionsRegistered(): TPromise { @@ -451,8 +459,8 @@ export class ExtensionService extends Disposable implements IExtensionService { } public getExtensionsStatus(): { [id: string]: IExtensionsStatus; } { - const activationTimes = this._extensionHostProcessManager.getActivationTimes(); - const runtimeErrors = this._extensionHostProcessManager.getRuntimeErrors(); + const activationTimes = this._extensionHostProcessManager ? this._extensionHostProcessManager.getActivationTimes() : {}; + const runtimeErrors = this._extensionHostProcessManager ? this._extensionHostProcessManager.getRuntimeErrors() : {}; let result: { [id: string]: IExtensionsStatus; } = Object.create(null); if (this._registry) { @@ -471,7 +479,10 @@ export class ExtensionService extends Disposable implements IExtensionService { } public canProfileExtensionHost(): boolean { - return this._extensionHostProcessManager.canProfileExtensionHost(); + if (this._extensionHostProcessManager) { + return this._extensionHostProcessManager.canProfileExtensionHost(); + } + return false; } public startExtensionHostProfile(): TPromise { @@ -487,7 +498,7 @@ export class ExtensionService extends Disposable implements IExtensionService { private _scanAndHandleExtensions(): void { - this._getRuntimeExtension() + this._getRuntimeExtensions() .then(runtimeExtensons => { this._registry = new ExtensionDescriptionRegistry(runtimeExtensons); @@ -512,7 +523,7 @@ export class ExtensionService extends Disposable implements IExtensionService { }); } - private _getRuntimeExtension(): TPromise { + private _getRuntimeExtensions(): TPromise { const log = new Logger((severity, source, message) => { this._logOrShowMessage(severity, this._isDev ? messageWithSource2(source, message) : message); }); @@ -576,7 +587,36 @@ export class ExtensionService extends Disposable implements IExtensionService { return runtimeExtensions; } }); - }); + }).then(extensions => this._updateEnableProposedApi(extensions)); + } + + private _updateEnableProposedApi(extensions: IExtensionDescription[]): IExtensionDescription[] { + const enableProposedApiForAll = !this._environmentService.isBuilt || (!!this._environmentService.extensionDevelopmentPath && product.nameLong.indexOf('Insiders') >= 0); + const enableProposedApiFor = this._environmentService.args['enable-proposed-api'] || []; + for (const extension of extensions) { + if (!isFalsyOrEmpty(product.extensionAllowedProposedApi) + && product.extensionAllowedProposedApi.indexOf(extension.id) >= 0 + ) { + // fast lane -> proposed api is available to all extensions + // that are listed in product.json-files + extension.enableProposedApi = true; + + } else if (extension.enableProposedApi && !extension.isBuiltin) { + if ( + !enableProposedApiForAll && + enableProposedApiFor.indexOf(extension.id) < 0 + ) { + extension.enableProposedApi = false; + console.error(`Extension '${extension.id} cannot use PROPOSED API (must started out of dev or enabled via --enable-proposed-api)`); + + } else { + // proposed api is available when developing or when an extension was explicitly + // spelled out via a command line argument + console.warn(`Extension '${extension.id}' uses PROPOSED API which is subject to change and removal without notice.`); + } + } + } + return extensions; } private _handleExtensionPointMessage(msg: IMessage) { @@ -634,11 +674,14 @@ export class ExtensionService extends Disposable implements IExtensionService { console.error(err); } - notificationService.prompt(Severity.Error, nls.localize('extensionCache.invalid', "Extensions have been modified on disk. Please reload the window."), [nls.localize('reloadWindow', "Reload Window")]).then(choice => { - if (choice === 0) { - windowService.reloadWindow(); - } - }); + notificationService.prompt( + Severity.Error, + nls.localize('extensionCache.invalid', "Extensions have been modified on disk. Please reload the window."), + [{ + label: nls.localize('reloadWindow', "Reload Window"), + run: () => windowService.reloadWindow() + }] + ); } private static async _readExtensionCache(environmentService: IEnvironmentService, cacheKey: string): TPromise { diff --git a/src/vs/workbench/services/extensions/node/extensionDescriptionRegistry.ts b/src/vs/workbench/services/extensions/node/extensionDescriptionRegistry.ts index 0d97bec7ccb..5fbb9357739 100644 --- a/src/vs/workbench/services/extensions/node/extensionDescriptionRegistry.ts +++ b/src/vs/workbench/services/extensions/node/extensionDescriptionRegistry.ts @@ -33,6 +33,12 @@ export class ExtensionDescriptionRegistry { if (Array.isArray(extensionDescription.activationEvents)) { for (let j = 0, lenJ = extensionDescription.activationEvents.length; j < lenJ; j++) { let activationEvent = extensionDescription.activationEvents[j]; + + // TODO@joao: there's no easy way to contribute this + if (activationEvent === 'onExternalUri') { + activationEvent = `onExternalUri:${extensionDescription.id}`; + } + this._activationMap[activationEvent] = this._activationMap[activationEvent] || []; this._activationMap[activationEvent].push(extensionDescription); } diff --git a/src/vs/workbench/services/extensions/node/extensionPoints.ts b/src/vs/workbench/services/extensions/node/extensionPoints.ts index cdcc36031bc..2707535072c 100644 --- a/src/vs/workbench/services/extensions/node/extensionPoints.ts +++ b/src/vs/workbench/services/extensions/node/extensionPoints.ts @@ -492,7 +492,7 @@ export class ExtensionScanner { /** * Read the extension defined in `absoluteFolderPath` */ - private static scanExtension(version: string, log: ILog, absoluteFolderPath: string, isBuiltin: boolean, nlsConfig: NlsConfiguration): TPromise { + public static scanExtension(version: string, log: ILog, absoluteFolderPath: string, isBuiltin: boolean, nlsConfig: NlsConfiguration): TPromise { absoluteFolderPath = normalize(absoluteFolderPath); let parser = new ExtensionManifestParser(version, log, absoluteFolderPath, isBuiltin); diff --git a/src/vs/workbench/services/extensions/node/rpcProtocol.ts b/src/vs/workbench/services/extensions/node/rpcProtocol.ts index 2e1ae6d2ee8..b40e5d2d19b 100644 --- a/src/vs/workbench/services/extensions/node/rpcProtocol.ts +++ b/src/vs/workbench/services/extensions/node/rpcProtocol.ts @@ -10,11 +10,88 @@ import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc'; import { LazyPromise } from 'vs/workbench/services/extensions/node/lazyPromise'; import { ProxyIdentifier, IRPCProtocol } from 'vs/workbench/services/extensions/node/proxyIdentifier'; import { CharCode } from 'vs/base/common/charCode'; +import URI, { UriComponents } from 'vs/base/common/uri'; +import { MarshalledObject } from 'vs/base/common/marshalling'; declare var Proxy: any; // TODO@TypeScript +export interface IURITransformer { + transformIncoming(uri: UriComponents): UriComponents; + transformOutgoing(uri: URI): URI; +} + +function _transformOutgoingURIs(obj: any, transformer: IURITransformer, depth: number): any { + + if (!obj || depth > 200) { + return null; + } + + if (typeof obj === 'object') { + if (obj instanceof URI) { + return transformer.transformOutgoing(obj); + } + + // walk object (or array) + for (let key in obj) { + if (Object.hasOwnProperty.call(obj, key)) { + const r = _transformOutgoingURIs(obj[key], transformer, depth + 1); + if (r !== null) { + obj[key] = r; + } + } + } + } + + return null; +} + +function transformOutgoingURIs(obj: any, transformer: IURITransformer): any { + const result = _transformOutgoingURIs(obj, transformer, 0); + if (result === null) { + // no change + return obj; + } + return result; +} + +function _transformIncomingURIs(obj: any, transformer: IURITransformer, depth: number): any { + + if (!obj || depth > 200) { + return null; + } + + if (typeof obj === 'object') { + + if ((obj).$mid === 1) { + return transformer.transformIncoming(obj); + } + + // walk object (or array) + for (let key in obj) { + if (Object.hasOwnProperty.call(obj, key)) { + const r = _transformIncomingURIs(obj[key], transformer, depth + 1); + if (r !== null) { + obj[key] = r; + } + } + } + } + + return null; +} + +function transformIncomingURIs(obj: any, transformer: IURITransformer): any { + const result = _transformIncomingURIs(obj, transformer, 0); + if (result === null) { + // no change + return obj; + } + return result; +} + export class RPCProtocol implements IRPCProtocol { + private readonly _uriTransformer: IURITransformer; private _isDisposed: boolean; private readonly _locals: { [id: string]: any; }; private readonly _proxies: { [id: string]: any; }; @@ -23,7 +100,8 @@ export class RPCProtocol implements IRPCProtocol { private readonly _pendingRPCReplies: { [msgId: string]: LazyPromise; }; private readonly _multiplexor: RPCMultiplexer; - constructor(protocol: IMessagePassingProtocol) { + constructor(protocol: IMessagePassingProtocol, transformer: IURITransformer = null) { + this._uriTransformer = transformer; this._isDisposed = false; this._locals = Object.create(null); this._proxies = Object.create(null); @@ -43,6 +121,13 @@ export class RPCProtocol implements IRPCProtocol { }); } + public transformIncomingURIs(obj: T): T { + if (!this._uriTransformer) { + return obj; + } + return transformIncomingURIs(obj, this._uriTransformer); + } + public getProxy(identifier: ProxyIdentifier): T { if (!this._proxies[identifier.id]) { this._proxies[identifier.id] = this._createProxy(identifier.id); @@ -84,6 +169,9 @@ export class RPCProtocol implements IRPCProtocol { } let msg = JSON.parse(rawmsg); + if (this._uriTransformer) { + msg = transformIncomingURIs(msg, this._uriTransformer); + } switch (msg.type) { case MessageType.Request: @@ -109,6 +197,9 @@ export class RPCProtocol implements IRPCProtocol { this._invokedHandlers[callId].then((r) => { delete this._invokedHandlers[callId]; + if (this._uriTransformer) { + r = transformOutgoingURIs(r, this._uriTransformer); + } this._multiplexor.send(MessageFactory.replyOK(callId, r)); }, (err) => { delete this._invokedHandlers[callId]; @@ -185,6 +276,9 @@ export class RPCProtocol implements IRPCProtocol { }); this._pendingRPCReplies[callId] = result; + if (this._uriTransformer) { + args = transformOutgoingURIs(args, this._uriTransformer); + } this._multiplexor.send(MessageFactory.request(callId, proxyId, methodName, args)); return result; } diff --git a/src/vs/workbench/services/files/electron-browser/encoding.ts b/src/vs/workbench/services/files/electron-browser/encoding.ts new file mode 100644 index 00000000000..c1b99f40985 --- /dev/null +++ b/src/vs/workbench/services/files/electron-browser/encoding.ts @@ -0,0 +1,145 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { WORKSPACE_EXTENSION } from 'vs/platform/workspaces/common/workspaces'; +import * as encoding from 'vs/base/node/encoding'; +import uri from 'vs/base/common/uri'; +import { IResolveContentOptions, isParent, IResourceEncodings } from 'vs/platform/files/common/files'; +import { isLinux } from 'vs/base/common/platform'; +import { join, extname } from 'path'; +import { ITextResourceConfigurationService } from 'vs/editor/common/services/resourceConfiguration'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { IDisposable, dispose } from 'vs/base/common/lifecycle'; + +export interface IEncodingOverride { + parent?: uri; + extension?: string; + encoding: string; +} + +// TODO@Ben debt - encodings should move one layer up from the file service into the text file +// service and then ideally be passed in as option to the file service +// the file service should talk about string | Buffer for reading and writing and only convert +// to strings if a encoding is provided +export class ResourceEncodings implements IResourceEncodings { + private encodingOverride: IEncodingOverride[]; + private toDispose: IDisposable[]; + + constructor( + private textResourceConfigurationService: ITextResourceConfigurationService, + private environmentService: IEnvironmentService, + private contextService: IWorkspaceContextService, + encodingOverride?: IEncodingOverride[] + ) { + this.encodingOverride = encodingOverride || this.getEncodingOverrides(); + this.toDispose = []; + + this.registerListeners(); + } + + private registerListeners(): void { + + // Workspace Folder Change + this.toDispose.push(this.contextService.onDidChangeWorkspaceFolders(() => { + this.encodingOverride = this.getEncodingOverrides(); + })); + } + + public getReadEncoding(resource: uri, options: IResolveContentOptions, detected: encoding.IDetectedEncodingResult): string { + let preferredEncoding: string; + + // Encoding passed in as option + if (options && options.encoding) { + if (detected.encoding === encoding.UTF8 && options.encoding === encoding.UTF8) { + preferredEncoding = encoding.UTF8_with_bom; // indicate the file has BOM if we are to resolve with UTF 8 + } else { + preferredEncoding = options.encoding; // give passed in encoding highest priority + } + } + + // Encoding detected + else if (detected.encoding) { + if (detected.encoding === encoding.UTF8) { + preferredEncoding = encoding.UTF8_with_bom; // if we detected UTF-8, it can only be because of a BOM + } else { + preferredEncoding = detected.encoding; + } + } + + // Encoding configured + else if (this.textResourceConfigurationService.getValue(resource, 'files.encoding') === encoding.UTF8_with_bom) { + preferredEncoding = encoding.UTF8; // if we did not detect UTF 8 BOM before, this can only be UTF 8 then + } + + return this.getEncodingForResource(resource, preferredEncoding); + } + + public getWriteEncoding(resource: uri, preferredEncoding?: string): string { + return this.getEncodingForResource(resource, preferredEncoding); + } + + private getEncodingForResource(resource: uri, preferredEncoding?: string): string { + let fileEncoding: string; + + const override = this.getEncodingOverride(resource); + if (override) { + fileEncoding = override; // encoding override always wins + } else if (preferredEncoding) { + fileEncoding = preferredEncoding; // preferred encoding comes second + } else { + fileEncoding = this.textResourceConfigurationService.getValue(resource, 'files.encoding'); // and last we check for settings + } + + if (!fileEncoding || !encoding.encodingExists(fileEncoding)) { + fileEncoding = encoding.UTF8; // the default is UTF 8 + } + + return fileEncoding; + } + + private getEncodingOverrides(): IEncodingOverride[] { + const encodingOverride: IEncodingOverride[] = []; + + // Global settings + encodingOverride.push({ parent: uri.file(this.environmentService.appSettingsHome), encoding: encoding.UTF8 }); + + // Workspace files + encodingOverride.push({ extension: WORKSPACE_EXTENSION, encoding: encoding.UTF8 }); + + // Folder Settings + this.contextService.getWorkspace().folders.forEach(folder => { + encodingOverride.push({ parent: uri.file(join(folder.uri.fsPath, '.vscode')), encoding: encoding.UTF8 }); + }); + + return encodingOverride; + } + + private getEncodingOverride(resource: uri): string { + if (resource && this.encodingOverride && this.encodingOverride.length) { + for (let i = 0; i < this.encodingOverride.length; i++) { + const override = this.encodingOverride[i]; + + // check if the resource is child of encoding override path + if (override.parent && isParent(resource.fsPath, override.parent.fsPath, !isLinux /* ignorecase */)) { + return override.encoding; + } + + // check if the resource extension is equal to encoding override + if (override.extension && extname(resource.fsPath) === `.${override.extension}`) { + return override.encoding; + } + } + } + + return null; + } + + public dispose(): void { + this.toDispose = dispose(this.toDispose); + } +} \ No newline at end of file diff --git a/src/vs/workbench/services/files/electron-browser/fileService.ts b/src/vs/workbench/services/files/electron-browser/fileService.ts index fc90d82cce0..f1be736dffc 100644 --- a/src/vs/workbench/services/files/electron-browser/fileService.ts +++ b/src/vs/workbench/services/files/electron-browser/fileService.ts @@ -2,91 +2,198 @@ * 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 nls from 'vs/nls'; +import * as paths from 'path'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as crypto from 'crypto'; +import * as assert from 'assert'; +import { isParent, FileOperation, FileOperationEvent, IContent, IFileService, IResolveFileOptions, IResolveFileResult, IResolveContentOptions, IFileStat, IStreamContent, FileOperationError, FileOperationResult, IUpdateContentOptions, FileChangeType, FileChangesEvent, ICreateFileOptions, IContentData, ITextSnapshot, IFilesConfiguration, IFileSystemProviderRegistrationEvent, IFileSystemProvider } from 'vs/platform/files/common/files'; +import { MAX_FILE_SIZE, MAX_HEAP_SIZE } from 'vs/platform/files/node/files'; +import { isEqualOrParent } from 'vs/base/common/paths'; +import { ResourceMap } from 'vs/base/common/map'; +import * as arrays from 'vs/base/common/arrays'; import { TPromise } from 'vs/base/common/winjs.base'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import * as paths from 'vs/base/common/paths'; -import * as encoding from 'vs/base/node/encoding'; -import * as errors from 'vs/base/common/errors'; +import * as objects from 'vs/base/common/objects'; +import * as extfs from 'vs/base/node/extfs'; +import { nfcall, ThrottledDelayer } from 'vs/base/common/async'; import uri from 'vs/base/common/uri'; -import { FileOperation, FileOperationEvent, IFileService, IFilesConfiguration, IResolveFileOptions, IFileStat, IResolveFileResult, IContent, IStreamContent, IImportResult, IResolveContentOptions, IUpdateContentOptions, FileChangesEvent, ICreateFileOptions, ITextSnapshot } from 'vs/platform/files/common/files'; -import { FileService as NodeFileService, IFileServiceOptions, IEncodingOverride } from 'vs/workbench/services/files/node/fileService'; -import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; -import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; +import * as nls from 'vs/nls'; +import { isWindows, isLinux, isMacintosh } from 'vs/base/common/platform'; +import { dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; +import * as pfs from 'vs/base/node/pfs'; +import * as encoding from 'vs/base/node/encoding'; +import * as flow from 'vs/base/node/flow'; +import { FileWatcher as UnixWatcherService } from 'vs/workbench/services/files/node/watcher/unix/watcherService'; +import { FileWatcher as WindowsWatcherService } from 'vs/workbench/services/files/node/watcher/win32/watcherService'; +import { toFileChangesEvent, normalize, IRawFileChange } from 'vs/workbench/services/files/node/watcher/common'; import { Event, Emitter } from 'vs/base/common/event'; -import { shell } from 'electron'; +import { FileWatcher as NsfwWatcherService } from 'vs/workbench/services/files/node/watcher/nsfw/watcherService'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/resourceConfiguration'; -import { isMacintosh, isWindows } from 'vs/base/common/platform'; -import product from 'vs/platform/node/product'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { getBaseLabel } from 'vs/base/common/labels'; import { Schemas } from 'vs/base/common/network'; -import { Severity, INotificationService, PromptOption } from 'vs/platform/notification/common/notification'; +import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; +import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; +import { onUnexpectedError } from 'vs/base/common/errors'; +import product from 'vs/platform/node/product'; +import { shell } from 'electron'; +import { IEncodingOverride, ResourceEncodings } from 'vs/workbench/services/files/electron-browser/encoding'; +import { createReadableOfSnapshot } from 'vs/workbench/services/files/electron-browser/streams'; + +class BufferPool { + + static _64K = new BufferPool(64 * 1024, 5); + + constructor( + readonly bufferSize: number, + private readonly _capacity: number, + private readonly _free: Buffer[] = [], + ) { } + + acquire(): Buffer { + if (this._free.length === 0) { + return Buffer.allocUnsafe(this.bufferSize); + } else { + return this._free.shift(); + } + } + + release(buf: Buffer): void { + if (this._free.length <= this._capacity) { + this._free.push(buf); + } + } +} + +export interface IFileServiceTestOptions { + disableWatcher?: boolean; + encodingOverride?: IEncodingOverride[]; +} export class FileService implements IFileService { public _serviceBrand: any; - // If we run with .NET framework < 4.5, we need to detect this error to inform the user + private static readonly FS_EVENT_DELAY = 50; // aggregate and only emit events when changes have stopped for this duration (in ms) + private static readonly FS_REWATCH_DELAY = 300; // delay to rewatch a file that was renamed or deleted (in ms) + private static readonly NET_VERSION_ERROR = 'System.MissingMethodException'; private static readonly NET_VERSION_ERROR_IGNORE_KEY = 'ignoreNetVersionError'; private static readonly ENOSPC_ERROR = 'ENOSPC'; private static readonly ENOSPC_ERROR_IGNORE_KEY = 'ignoreEnospcError'; - private raw: NodeFileService; + protected readonly _onFileChanges: Emitter; + protected readonly _onAfterOperation: Emitter; + protected readonly _onDidChangeFileSystemProviderRegistrations = new Emitter(); - private toUnbind: IDisposable[]; + protected toDispose: IDisposable[]; - protected _onFileChanges: Emitter; - protected _onAfterOperation: Emitter; + private activeWorkspaceFileChangeWatcher: IDisposable; + private activeFileChangesWatchers: ResourceMap; + private fileChangesWatchDelayer: ThrottledDelayer; + private undeliveredRawFileChangesEvents: IRawFileChange[]; + + private _encoding: ResourceEncodings; constructor( - @IConfigurationService private configurationService: IConfigurationService, - @IWorkspaceContextService private contextService: IWorkspaceContextService, - @IEnvironmentService private environmentService: IEnvironmentService, - @ILifecycleService private lifecycleService: ILifecycleService, - @INotificationService private notificationService: INotificationService, - @IStorageService private storageService: IStorageService, - @ITextResourceConfigurationService textResourceConfigurationService: ITextResourceConfigurationService + private contextService: IWorkspaceContextService, + private environmentService: IEnvironmentService, + private textResourceConfigurationService: ITextResourceConfigurationService, + private configurationService: IConfigurationService, + private lifecycleService: ILifecycleService, + private storageService: IStorageService, + private notificationService: INotificationService, + private options: IFileServiceTestOptions = Object.create(null) ) { - this.toUnbind = []; + this.toDispose = []; this._onFileChanges = new Emitter(); - this.toUnbind.push(this._onFileChanges); + this.toDispose.push(this._onFileChanges); this._onAfterOperation = new Emitter(); - this.toUnbind.push(this._onAfterOperation); + this.toDispose.push(this._onAfterOperation); - const configuration = this.configurationService.getValue(); + this.activeFileChangesWatchers = new ResourceMap(); + this.fileChangesWatchDelayer = new ThrottledDelayer(FileService.FS_EVENT_DELAY); + this.undeliveredRawFileChangesEvents = []; - let watcherIgnoredPatterns: string[] = []; - if (configuration.files && configuration.files.watcherExclude) { - watcherIgnoredPatterns = Object.keys(configuration.files.watcherExclude).filter(k => !!configuration.files.watcherExclude[k]); + this._encoding = new ResourceEncodings(textResourceConfigurationService, environmentService, contextService, this.options.encodingOverride); + + this.registerListeners(); + } + + public get encoding(): ResourceEncodings { + return this._encoding; + } + + private registerListeners(): void { + + // Wait until we are fully running before starting file watchers + this.lifecycleService.when(LifecyclePhase.Running).then(() => { + this.setupFileWatching(); + }); + + // Workbench State Change + this.toDispose.push(this.contextService.onDidChangeWorkbenchState(() => { + if (this.lifecycleService.phase >= LifecyclePhase.Running) { + this.setupFileWatching(); + } + })); + + // Lifecycle + this.lifecycleService.onShutdown(this.dispose, this); + } + + private handleError(error: string | Error): void { + const msg = error ? error.toString() : void 0; + if (!msg) { + return; } - // build config - const fileServiceConfig: IFileServiceOptions = { - errorLogger: (msg: string) => this.onFileServiceError(msg), - encodingOverride: this.getEncodingOverrides(), - watcherIgnoredPatterns, - verboseLogging: environmentService.verbose, - useExperimentalFileWatcher: configuration.files.useExperimentalFileWatcher, - elevationSupport: { - cliPath: this.environmentService.cliPath, - promptTitle: this.environmentService.appNameLong, - promptIcnsPath: (isMacintosh && this.environmentService.isBuilt) ? paths.join(paths.dirname(this.environmentService.appRoot), `${product.nameShort}.icns`) : void 0 - } - }; + // Forward to unexpected error handler + onUnexpectedError(msg); - // create service - this.raw = new NodeFileService(contextService, environmentService, textResourceConfigurationService, configurationService, lifecycleService, fileServiceConfig); + // Detect if we run < .NET Framework 4.5 (TODO@ben remove with new watcher impl) + if (msg.indexOf(FileService.NET_VERSION_ERROR) >= 0 && !this.storageService.getBoolean(FileService.NET_VERSION_ERROR_IGNORE_KEY, StorageScope.WORKSPACE)) { + this.notificationService.prompt( + Severity.Warning, + nls.localize('netVersionError', "The Microsoft .NET Framework 4.5 is required. Please follow the link to install it."), + [{ + label: nls.localize('installNet', "Download .NET Framework 4.5"), + run: () => window.open('https://go.microsoft.com/fwlink/?LinkId=786533') + }, + { + label: nls.localize('neverShowAgain', "Don't Show Again"), + isSecondary: true, + run: () => this.storageService.store(FileService.NET_VERSION_ERROR_IGNORE_KEY, true, StorageScope.WORKSPACE) + }] + ); + } - // Listeners - this.registerListeners(); + // Detect if we run into ENOSPC issues + if (msg.indexOf(FileService.ENOSPC_ERROR) >= 0 && !this.storageService.getBoolean(FileService.ENOSPC_ERROR_IGNORE_KEY, StorageScope.WORKSPACE)) { + this.notificationService.prompt( + Severity.Warning, + nls.localize('enospcError', "{0} is unable to watch for file changes in this large workspace. Please follow the instructions link to resolve this issue.", product.nameLong), + [{ + label: nls.localize('learnMore', "Instructions"), + run: () => window.open('https://go.microsoft.com/fwlink/?linkid=867693') + }, + { + label: nls.localize('neverShowAgain', "Don't Show Again"), + isSecondary: true, + run: () => this.storageService.store(FileService.ENOSPC_ERROR_IGNORE_KEY, true, StorageScope.WORKSPACE) + }] + ); + } } public get onFileChanges(): Event { @@ -97,132 +204,704 @@ export class FileService implements IFileService { return this._onAfterOperation.event; } - private onFileServiceError(error: string | Error): void { - const msg = error ? error.toString() : void 0; - if (!msg) { + private setupFileWatching(): void { + + // dispose old if any + if (this.activeWorkspaceFileChangeWatcher) { + this.activeWorkspaceFileChangeWatcher.dispose(); + } + + // Return if not aplicable + const workbenchState = this.contextService.getWorkbenchState(); + if (workbenchState === WorkbenchState.EMPTY || this.options.disableWatcher) { return; } - // Forward to unexpected error handler - errors.onUnexpectedError(msg); - - // Detect if we run < .NET Framework 4.5 (TODO@ben remove with new watcher impl) - if (msg.indexOf(FileService.NET_VERSION_ERROR) >= 0 && !this.storageService.getBoolean(FileService.NET_VERSION_ERROR_IGNORE_KEY, StorageScope.WORKSPACE)) { - const choices: PromptOption[] = [nls.localize('installNet', "Download .NET Framework 4.5"), { label: nls.localize('neverShowAgain', "Don't Show Again") }]; - this.notificationService.prompt(Severity.Warning, nls.localize('netVersionError', "The Microsoft .NET Framework 4.5 is required. Please follow the link to install it."), choices).then(choice => { - switch (choice) { - case 0 /* Read More */: - window.open('https://go.microsoft.com/fwlink/?LinkId=786533'); - break; - case 1 /* Never show again */: - this.storageService.store(FileService.NET_VERSION_ERROR_IGNORE_KEY, true, StorageScope.WORKSPACE); - break; - } - }); + // new watcher: use it if setting tells us so or we run in multi-root environment + const configuration = this.configurationService.getValue(); + if ((configuration.files && configuration.files.useExperimentalFileWatcher) || workbenchState === WorkbenchState.WORKSPACE) { + const multiRootWatcher = new NsfwWatcherService(this.contextService, this.configurationService, e => this._onFileChanges.fire(e), err => this.handleError(err), this.environmentService.verbose); + this.activeWorkspaceFileChangeWatcher = toDisposable(multiRootWatcher.startWatching()); } - // Detect if we run into ENOSPC issues - if (msg.indexOf(FileService.ENOSPC_ERROR) >= 0 && !this.storageService.getBoolean(FileService.ENOSPC_ERROR_IGNORE_KEY, StorageScope.WORKSPACE)) { - const choices: PromptOption[] = [nls.localize('learnMore', "Instructions"), { label: nls.localize('neverShowAgain', "Don't Show Again") }]; - this.notificationService.prompt(Severity.Warning, nls.localize('enospcError', "{0} is unable to watch for file changes in this large workspace. Please follow the instructions link to resolve this issue.", product.nameLong), choices).then(choice => { - switch (choice) { - case 0 /* Read More */: - window.open('https://go.microsoft.com/fwlink/?linkid=867693'); - break; - case 1 /* Never show again */: - this.storageService.store(FileService.ENOSPC_ERROR_IGNORE_KEY, true, StorageScope.WORKSPACE); - break; - } - }); + // legacy watcher + else { + let watcherIgnoredPatterns: string[] = []; + if (configuration.files && configuration.files.watcherExclude) { + watcherIgnoredPatterns = Object.keys(configuration.files.watcherExclude).filter(k => !!configuration.files.watcherExclude[k]); + } + + if (isWindows) { + const legacyWindowsWatcher = new WindowsWatcherService(this.contextService, watcherIgnoredPatterns, e => this._onFileChanges.fire(e), err => this.handleError(err), this.environmentService.verbose); + this.activeWorkspaceFileChangeWatcher = toDisposable(legacyWindowsWatcher.startWatching()); + } else { + const legacyUnixWatcher = new UnixWatcherService(this.contextService, watcherIgnoredPatterns, e => this._onFileChanges.fire(e), err => this.handleError(err), this.environmentService.verbose); + this.activeWorkspaceFileChangeWatcher = toDisposable(legacyUnixWatcher.startWatching()); + } } } - private registerListeners(): void { + public readonly onDidChangeFileSystemProviderRegistrations: Event = this._onDidChangeFileSystemProviderRegistrations.event; - // File events - this.toUnbind.push(this.raw.onFileChanges(e => this._onFileChanges.fire(e))); - this.toUnbind.push(this.raw.onAfterOperation(e => this._onAfterOperation.fire(e))); - - // Config changes - this.toUnbind.push(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationChange(e))); - - // Root changes - this.toUnbind.push(this.contextService.onDidChangeWorkspaceFolders(() => this.onDidChangeWorkspaceFolders())); - - // Lifecycle - this.lifecycleService.onShutdown(this.dispose, this); + public registerProvider(scheme: string, provider: IFileSystemProvider): IDisposable { + throw new Error('not implemented'); } - private onDidChangeWorkspaceFolders(): void { - this.updateOptions({ encodingOverride: this.getEncodingOverrides() }); - } - - private getEncodingOverrides(): IEncodingOverride[] { - const encodingOverride: IEncodingOverride[] = []; - encodingOverride.push({ resource: uri.file(this.environmentService.appSettingsHome), encoding: encoding.UTF8 }); - this.contextService.getWorkspace().folders.forEach(folder => { - encodingOverride.push({ resource: uri.file(paths.join(folder.uri.fsPath, '.vscode')), encoding: encoding.UTF8 }); - }); - - return encodingOverride; - } - - private onConfigurationChange(event: IConfigurationChangeEvent): void { - if (event.affectsConfiguration('files.useExperimentalFileWatcher')) { - this.updateOptions({ useExperimentalFileWatcher: this.configurationService.getValue('files.useExperimentalFileWatcher') }); - } - } - - public updateOptions(options: object): void { - this.raw.updateOptions(options); + public canHandleResource(resource: uri): boolean { + return resource.scheme === Schemas.file; } public resolveFile(resource: uri, options?: IResolveFileOptions): TPromise { - return this.raw.resolveFile(resource, options); + return this.resolve(resource, options); } public resolveFiles(toResolve: { resource: uri, options?: IResolveFileOptions }[]): TPromise { - return this.raw.resolveFiles(toResolve); + return TPromise.join(toResolve.map(resourceAndOptions => this.resolve(resourceAndOptions.resource, resourceAndOptions.options) + .then(stat => ({ stat, success: true }), error => ({ stat: void 0, success: false })))); } public existsFile(resource: uri): TPromise { - return this.raw.existsFile(resource); + return this.resolveFile(resource).then(() => true, () => false); } public resolveContent(resource: uri, options?: IResolveContentOptions): TPromise { - return this.raw.resolveContent(resource, options); + return this.resolveStreamContent(resource, options).then(streamContent => { + return new TPromise((resolve, reject) => { + + const result: IContent = { + resource: streamContent.resource, + name: streamContent.name, + mtime: streamContent.mtime, + etag: streamContent.etag, + encoding: streamContent.encoding, + value: '' + }; + + streamContent.value.on('data', chunk => result.value += chunk); + streamContent.value.on('error', err => reject(err)); + streamContent.value.on('end', _ => resolve(result)); + + return result; + }); + }); } public resolveStreamContent(resource: uri, options?: IResolveContentOptions): TPromise { - return this.raw.resolveStreamContent(resource, options); + + // Guard early against attempts to resolve an invalid file path + if (resource.scheme !== Schemas.file || !resource.fsPath) { + return TPromise.wrapError(new FileOperationError( + nls.localize('fileInvalidPath', "Invalid file resource ({0})", resource.toString(true)), + FileOperationResult.FILE_INVALID_PATH, + options + )); + } + + const result: IStreamContent = { + resource: void 0, + name: void 0, + mtime: void 0, + etag: void 0, + encoding: void 0, + value: void 0 + }; + + const contentResolverTokenSource = new CancellationTokenSource(); + + const onStatError = (error: Error) => { + + // error: stop reading the file the stat and content resolve call + // usually race, mostly likely the stat call will win and cancel + // the content call + contentResolverTokenSource.cancel(); + + // forward error + return TPromise.wrapError(error); + }; + + const statsPromise = this.resolveFile(resource).then(stat => { + result.resource = stat.resource; + result.name = stat.name; + result.mtime = stat.mtime; + result.etag = stat.etag; + + // Return early if resource is a directory + if (stat.isDirectory) { + return onStatError(new FileOperationError( + nls.localize('fileIsDirectoryError', "File is directory"), + FileOperationResult.FILE_IS_DIRECTORY, + options + )); + } + + // Return early if file not modified since + if (options && options.etag && options.etag === stat.etag) { + return onStatError(new FileOperationError( + nls.localize('fileNotModifiedError', "File not modified since"), + FileOperationResult.FILE_NOT_MODIFIED_SINCE, + options + )); + } + + // Return early if file is too large to load + if (typeof stat.size === 'number') { + if (stat.size > Math.max(this.environmentService.args['max-memory'] * 1024 * 1024 || 0, MAX_HEAP_SIZE)) { + return onStatError(new FileOperationError( + nls.localize('fileTooLargeForHeapError', "To open a file of this size, you need to restart VS Code and allow it to use more memory"), + FileOperationResult.FILE_EXCEED_MEMORY_LIMIT + )); + } + + if (stat.size > MAX_FILE_SIZE) { + return onStatError(new FileOperationError( + nls.localize('fileTooLargeError', "File too large to open"), + FileOperationResult.FILE_TOO_LARGE + )); + } + } + + return void 0; + }, err => { + + // Wrap file not found errors + if (err.code === 'ENOENT') { + return onStatError(new FileOperationError( + nls.localize('fileNotFoundError', "File not found ({0})", resource.toString(true)), + FileOperationResult.FILE_NOT_FOUND, + options + )); + } + + return onStatError(err); + }); + + let completePromise: Thenable; + + // await the stat iff we already have an etag so that we compare the + // etag from the stat before we actually read the file again. + if (options && options.etag) { + completePromise = statsPromise.then(() => { + return this.fillInContents(result, resource, options, contentResolverTokenSource.token); // Waterfall -> only now resolve the contents + }); + } + + // a fresh load without a previous etag which means we can resolve the file stat + // and the content at the same time, avoiding the waterfall. + else { + completePromise = Promise.all([statsPromise, this.fillInContents(result, resource, options, contentResolverTokenSource.token)]); + } + + return TPromise.wrap(completePromise).then(() => { + contentResolverTokenSource.dispose(); + + return result; + }); } - public updateContent(resource: uri, value: string | ITextSnapshot, options?: IUpdateContentOptions): TPromise { - return this.raw.updateContent(resource, value, options); + private fillInContents(content: IStreamContent, resource: uri, options: IResolveContentOptions, token: CancellationToken): Thenable { + return this.resolveFileData(resource, options, token).then(data => { + content.encoding = data.encoding; + content.value = data.stream; + }); } - public moveFile(source: uri, target: uri, overwrite?: boolean): TPromise { - return this.raw.moveFile(source, target, overwrite); + private resolveFileData(resource: uri, options: IResolveContentOptions, token: CancellationToken): Thenable { + + const chunkBuffer = BufferPool._64K.acquire(); + + const result: IContentData = { + encoding: void 0, + stream: void 0 + }; + + return new Promise((resolve, reject) => { + fs.open(this.toAbsolutePath(resource), 'r', (err, fd) => { + if (err) { + if (err.code === 'ENOENT') { + // Wrap file not found errors + err = new FileOperationError( + nls.localize('fileNotFoundError', "File not found ({0})", resource.toString(true)), + FileOperationResult.FILE_NOT_FOUND, + options + ); + } + + return reject(err); + } + + let decoder: NodeJS.ReadWriteStream; + let totalBytesRead = 0; + + const finish = (err?: any) => { + + if (err) { + if (err.code === 'EISDIR') { + // Wrap EISDIR errors (fs.open on a directory works, but you cannot read from it) + err = new FileOperationError( + nls.localize('fileIsDirectoryError', "File is directory"), + FileOperationResult.FILE_IS_DIRECTORY, + options + ); + } + if (decoder) { + // If the decoder already started, we have to emit the error through it as + // event because the promise is already resolved! + decoder.emit('error', err); + } else { + reject(err); + } + } + if (decoder) { + decoder.end(); + } + + // return the shared buffer + BufferPool._64K.release(chunkBuffer); + + if (fd) { + fs.close(fd, err => { + if (err) { + this.handleError(`resolveFileData#close(): ${err.toString()}`); + } + }); + } + }; + + const handleChunk = (bytesRead: number) => { + if (token.isCancellationRequested) { + // cancellation -> finish + finish(new Error('cancelled')); + } else if (bytesRead === 0) { + // no more data -> finish + finish(); + } else if (bytesRead < chunkBuffer.length) { + // write the sub-part of data we received -> repeat + decoder.write(chunkBuffer.slice(0, bytesRead), readChunk); + } else { + // write all data we received -> repeat + decoder.write(chunkBuffer, readChunk); + } + }; + + let currentPosition: number = (options && options.position) || null; + + const readChunk = () => { + fs.read(fd, chunkBuffer, 0, chunkBuffer.length, currentPosition, (err, bytesRead) => { + totalBytesRead += bytesRead; + + if (typeof currentPosition === 'number') { + // if we received a position argument as option we need to ensure that + // we advance the position by the number of bytesread + currentPosition += bytesRead; + } + + if (totalBytesRead > Math.max(this.environmentService.args['max-memory'] * 1024 * 1024 || 0, MAX_HEAP_SIZE)) { + finish(new FileOperationError( + nls.localize('fileTooLargeForHeapError', "To open a file of this size, you need to restart VS Code and allow it to use more memory"), + FileOperationResult.FILE_EXCEED_MEMORY_LIMIT + )); + } + + if (totalBytesRead > MAX_FILE_SIZE) { + // stop when reading too much + finish(new FileOperationError( + nls.localize('fileTooLargeError', "File too large to open"), + FileOperationResult.FILE_TOO_LARGE, + options + )); + } else if (err) { + // some error happened + finish(err); + + } else if (decoder) { + // pass on to decoder + handleChunk(bytesRead); + + } else { + // when receiving the first chunk of data we need to create the + // decoding stream which is then used to drive the string stream. + const autoGuessEncoding = (options && options.autoGuessEncoding) || this.textResourceConfigurationService.getValue(resource, 'files.autoGuessEncoding'); + TPromise.as(encoding.detectEncodingFromBuffer( + { buffer: chunkBuffer, bytesRead }, + autoGuessEncoding + )).then(detected => { + + if (options && options.acceptTextOnly && detected.seemsBinary) { + // Return error early if client only accepts text and this is not text + finish(new FileOperationError( + nls.localize('fileBinaryError', "File seems to be binary and cannot be opened as text"), + FileOperationResult.FILE_IS_BINARY, + options + )); + + } else { + result.encoding = this._encoding.getReadEncoding(resource, options, detected); + result.stream = decoder = encoding.decodeStream(result.encoding); + resolve(result); + handleChunk(bytesRead); + } + + }).then(void 0, err => { + // failed to get encoding + finish(err); + }); + } + }); + }; + + // start reading + readChunk(); + }); + }); } - public copyFile(source: uri, target: uri, overwrite?: boolean): TPromise { - return this.raw.copyFile(source, target, overwrite); + public updateContent(resource: uri, value: string | ITextSnapshot, options: IUpdateContentOptions = Object.create(null)): TPromise { + if (options.writeElevated) { + return this.doUpdateContentElevated(resource, value, options); + } + + return this.doUpdateContent(resource, value, options); } - public createFile(resource: uri, content?: string, options?: ICreateFileOptions): TPromise { - return this.raw.createFile(resource, content, options); + private doUpdateContent(resource: uri, value: string | ITextSnapshot, options: IUpdateContentOptions = Object.create(null)): TPromise { + const absolutePath = this.toAbsolutePath(resource); + + // 1.) check file for writing + return this.checkFileBeforeWriting(absolutePath, options).then(exists => { + let createParentsPromise: TPromise; + if (exists) { + createParentsPromise = TPromise.as(null); + } else { + createParentsPromise = pfs.mkdirp(paths.dirname(absolutePath)); + } + + // 2.) create parents as needed + return createParentsPromise.then(() => { + const encodingToWrite = this._encoding.getWriteEncoding(resource, options.encoding); + let addBomPromise: TPromise = TPromise.as(false); + + // UTF_16 BE and LE as well as UTF_8 with BOM always have a BOM + if (encodingToWrite === encoding.UTF16be || encodingToWrite === encoding.UTF16le || encodingToWrite === encoding.UTF8_with_bom) { + addBomPromise = TPromise.as(true); + } + + // Existing UTF-8 file: check for options regarding BOM + else if (exists && encodingToWrite === encoding.UTF8) { + if (options.overwriteEncoding) { + addBomPromise = TPromise.as(false); // if we are to overwrite the encoding, we do not preserve it if found + } else { + addBomPromise = encoding.detectEncodingByBOM(absolutePath).then(enc => enc === encoding.UTF8); // otherwise preserve it if found + } + } + + // 3.) check to add UTF BOM + return addBomPromise.then(addBom => { + + // 4.) set contents and resolve + return this.doSetContentsAndResolve(resource, absolutePath, value, addBom, encodingToWrite).then(void 0, error => { + if (!exists || error.code !== 'EPERM' || !isWindows) { + return TPromise.wrapError(error); + } + + // On Windows and if the file exists with an EPERM error, we try a different strategy of saving the file + // by first truncating the file and then writing with r+ mode. This helps to save hidden files on Windows + // (see https://github.com/Microsoft/vscode/issues/931) + + // 5.) truncate + return pfs.truncate(absolutePath, 0).then(() => { + + // 6.) set contents (this time with r+ mode) and resolve again + return this.doSetContentsAndResolve(resource, absolutePath, value, addBom, encodingToWrite, { flag: 'r+' }); + }); + }); + }); + }); + }).then(null, error => { + if (error.code === 'EACCES' || error.code === 'EPERM') { + return TPromise.wrapError(new FileOperationError( + nls.localize('filePermission', "Permission denied writing to file ({0})", resource.toString(true)), + FileOperationResult.FILE_PERMISSION_DENIED, + options + )); + } + + return TPromise.wrapError(error); + }); + } + + private doSetContentsAndResolve(resource: uri, absolutePath: string, value: string | ITextSnapshot, addBOM: boolean, encodingToWrite: string, options?: { mode?: number; flag?: string; }): TPromise { + let writeFilePromise: TPromise; + + // Configure encoding related options as needed + const writeFileOptions: extfs.IWriteFileOptions = options ? options : Object.create(null); + if (addBOM || encodingToWrite !== encoding.UTF8) { + writeFileOptions.encoding = { + charset: encodingToWrite, + addBOM + }; + } + + if (typeof value === 'string') { + writeFilePromise = pfs.writeFile(absolutePath, value, writeFileOptions); + } else { + writeFilePromise = pfs.writeFile(absolutePath, createReadableOfSnapshot(value), writeFileOptions); + } + + // set contents + return writeFilePromise.then(() => { + + // resolve + return this.resolve(resource); + }); + } + + private doUpdateContentElevated(resource: uri, value: string | ITextSnapshot, options: IUpdateContentOptions = Object.create(null)): TPromise { + const absolutePath = this.toAbsolutePath(resource); + + // 1.) check file for writing + return this.checkFileBeforeWriting(absolutePath, options, options.overwriteReadonly /* ignore readonly if we overwrite readonly, this is handled via sudo later */).then(exists => { + const writeOptions: IUpdateContentOptions = objects.assign(Object.create(null), options); + writeOptions.writeElevated = false; + writeOptions.encoding = this._encoding.getWriteEncoding(resource, options.encoding); + + // 2.) write to a temporary file to be able to copy over later + const tmpPath = paths.join(os.tmpdir(), `code-elevated-${Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 6)}`); + return this.updateContent(uri.file(tmpPath), value, writeOptions).then(() => { + + // 3.) invoke our CLI as super user + return (import('sudo-prompt')).then(sudoPrompt => { + return new TPromise((c, e) => { + const promptOptions = { + name: this.environmentService.appNameLong.replace('-', ''), + icns: (isMacintosh && this.environmentService.isBuilt) ? paths.join(paths.dirname(this.environmentService.appRoot), `${product.nameShort}.icns`) : void 0 + }; + + const sudoCommand: string[] = [`"${this.environmentService.cliPath}"`]; + if (options.overwriteReadonly) { + sudoCommand.push('--file-chmod'); + } + sudoCommand.push('--file-write', `"${tmpPath}"`, `"${absolutePath}"`); + + sudoPrompt.exec(sudoCommand.join(' '), promptOptions, (error: string, stdout: string, stderr: string) => { + if (error || stderr) { + e(error || stderr); + } else { + c(void 0); + } + }); + }); + }).then(() => { + + // 3.) delete temp file + return pfs.del(tmpPath, os.tmpdir()).then(() => { + + // 4.) resolve again + return this.resolve(resource); + }); + }); + }); + }).then(null, error => { + if (this.environmentService.verbose) { + this.handleError(`Unable to write to file '${resource.toString(true)}' as elevated user (${error})`); + } + + if (!FileOperationError.isFileOperationError(error)) { + error = new FileOperationError( + nls.localize('filePermission', "Permission denied writing to file ({0})", resource.toString(true)), + FileOperationResult.FILE_PERMISSION_DENIED, + options + ); + } + + return TPromise.wrapError(error); + }); + } + + public createFile(resource: uri, content: string = '', options: ICreateFileOptions = Object.create(null)): TPromise { + const absolutePath = this.toAbsolutePath(resource); + + let checkFilePromise: TPromise; + if (options.overwrite) { + checkFilePromise = TPromise.as(false); + } else { + checkFilePromise = pfs.exists(absolutePath); + } + + // Check file exists + return checkFilePromise.then(exists => { + if (exists && !options.overwrite) { + return TPromise.wrapError(new FileOperationError( + nls.localize('fileExists', "File to create already exists ({0})", resource.toString(true)), + FileOperationResult.FILE_MODIFIED_SINCE, + options + )); + } + + // Create file + return this.updateContent(resource, content).then(result => { + + // Events + this._onAfterOperation.fire(new FileOperationEvent(resource, FileOperation.CREATE, result)); + + return result; + }); + }); } public createFolder(resource: uri): TPromise { - return this.raw.createFolder(resource); + + // 1.) Create folder + const absolutePath = this.toAbsolutePath(resource); + return pfs.mkdirp(absolutePath).then(() => { + + // 2.) Resolve + return this.resolve(resource).then(result => { + + // Events + this._onAfterOperation.fire(new FileOperationEvent(resource, FileOperation.CREATE, result)); + + return result; + }); + }); } - public touchFile(resource: uri): TPromise { - return this.raw.touchFile(resource); + private checkFileBeforeWriting(absolutePath: string, options: IUpdateContentOptions = Object.create(null), ignoreReadonly?: boolean): TPromise { + return pfs.exists(absolutePath).then(exists => { + if (exists) { + return pfs.stat(absolutePath).then(stat => { + if (stat.isDirectory()) { + return TPromise.wrapError(new Error('Expected file is actually a directory')); + } + + // Dirty write prevention: if the file on disk has been changed and does not match our expected + // mtime and etag, we bail out to prevent dirty writing. + // + // First, we check for a mtime that is in the future before we do more checks. The assumption is + // that only the mtime is an indicator for a file that has changd on disk. + // + // Second, if the mtime has advanced, we compare the size of the file on disk with our previous + // one using the etag() function. Relying only on the mtime check has prooven to produce false + // positives due to file system weirdness (especially around remote file systems). As such, the + // check for size is a weaker check because it can return a false negative if the file has changed + // but to the same length. This is a compromise we take to avoid having to produce checksums of + // the file content for comparison which would be much slower to compute. + if (typeof options.mtime === 'number' && typeof options.etag === 'string' && options.mtime < stat.mtime.getTime() && options.etag !== etag(stat.size, options.mtime)) { + return TPromise.wrapError(new FileOperationError(nls.localize('fileModifiedError', "File Modified Since"), FileOperationResult.FILE_MODIFIED_SINCE, options)); + } + + // Throw if file is readonly and we are not instructed to overwrite + if (!ignoreReadonly && !(stat.mode & 128) /* readonly */) { + if (!options.overwriteReadonly) { + return this.readOnlyError(options); + } + + // Try to change mode to writeable + let mode = stat.mode; + mode = mode | 128; + return pfs.chmod(absolutePath, mode).then(() => { + + // Make sure to check the mode again, it could have failed + return pfs.stat(absolutePath).then(stat => { + if (!(stat.mode & 128) /* readonly */) { + return this.readOnlyError(options); + } + + return exists; + }); + }); + } + + return TPromise.as(exists); + }); + } + + return TPromise.as(exists); + }); + } + + private readOnlyError(options: IUpdateContentOptions): TPromise { + return TPromise.wrapError(new FileOperationError( + nls.localize('fileReadOnlyError', "File is Read Only"), + FileOperationResult.FILE_READ_ONLY, + options + )); } public rename(resource: uri, newName: string): TPromise { - return this.raw.rename(resource, newName); + const newPath = paths.join(paths.dirname(resource.fsPath), newName); + + return this.moveFile(resource, uri.file(newPath)); + } + + public moveFile(source: uri, target: uri, overwrite?: boolean): TPromise { + return this.moveOrCopyFile(source, target, false, overwrite); + } + + public copyFile(source: uri, target: uri, overwrite?: boolean): TPromise { + return this.moveOrCopyFile(source, target, true, overwrite); + } + + private moveOrCopyFile(source: uri, target: uri, keepCopy: boolean, overwrite: boolean): TPromise { + const sourcePath = this.toAbsolutePath(source); + const targetPath = this.toAbsolutePath(target); + + // 1.) move / copy + return this.doMoveOrCopyFile(sourcePath, targetPath, keepCopy, overwrite).then(() => { + + // 2.) resolve + return this.resolve(target).then(result => { + + // Events + this._onAfterOperation.fire(new FileOperationEvent(source, keepCopy ? FileOperation.COPY : FileOperation.MOVE, result)); + + return result; + }); + }); + } + + private doMoveOrCopyFile(sourcePath: string, targetPath: string, keepCopy: boolean, overwrite: boolean): TPromise { + + // 1.) validate operation + if (isParent(targetPath, sourcePath, !isLinux)) { + return TPromise.wrapError(new Error('Unable to move/copy when source path is parent of target path')); + } + + // 2.) check if target exists + return pfs.exists(targetPath).then(exists => { + const isCaseRename = sourcePath.toLowerCase() === targetPath.toLowerCase(); + const isSameFile = sourcePath === targetPath; + + // Return early with conflict if target exists and we are not told to overwrite + if (exists && !isCaseRename && !overwrite) { + return TPromise.wrapError(new FileOperationError(nls.localize('fileMoveConflict', "Unable to move/copy. File already exists at destination."), FileOperationResult.FILE_MOVE_CONFLICT)); + } + + // 3.) make sure target is deleted before we move/copy unless this is a case rename of the same file + let deleteTargetPromise = TPromise.wrap(void 0); + if (exists && !isCaseRename) { + if (isEqualOrParent(sourcePath, targetPath, !isLinux /* ignorecase */)) { + return TPromise.wrapError(new Error(nls.localize('unableToMoveCopyError', "Unable to move/copy. File would replace folder it is contained in."))); // catch this corner case! + } + + deleteTargetPromise = this.del(uri.file(targetPath)); + } + + return deleteTargetPromise.then(() => { + + // 4.) make sure parents exists + return pfs.mkdirp(paths.dirname(targetPath)).then(() => { + + // 4.) copy/move + if (isSameFile) { + return TPromise.wrap(null); + } else if (keepCopy) { + return nfcall(extfs.copy, sourcePath, targetPath); + } else { + return nfcall(extfs.mv, sourcePath, targetPath); + } + }).then(() => exists); + }); + }); } public del(resource: uri, useTrash?: boolean): TPromise { @@ -230,7 +909,7 @@ export class FileService implements IFileService { return this.doMoveItemToTrash(resource); } - return this.raw.del(resource); + return this.doDelete(resource); } private doMoveItemToTrash(resource: uri): TPromise { @@ -245,44 +924,315 @@ export class FileService implements IFileService { return TPromise.as(null); } - public importFile(source: uri, targetFolder: uri): TPromise { - return this.raw.importFile(source, targetFolder).then((result) => { - return { - isNew: result && result.isNew, - stat: result && result.stat - }; + private doDelete(resource: uri): TPromise { + const absolutePath = this.toAbsolutePath(resource); + + return pfs.del(absolutePath, os.tmpdir()).then(() => { + + // Events + this._onAfterOperation.fire(new FileOperationEvent(resource, FileOperation.DELETE)); + }); + } + + // Helpers + + private toAbsolutePath(arg1: uri | IFileStat): string { + let resource: uri; + if (arg1 instanceof uri) { + resource = arg1; + } else { + resource = (arg1).resource; + } + + assert.ok(resource && resource.scheme === Schemas.file, `Invalid resource: ${resource}`); + + return paths.normalize(resource.fsPath); + } + + private resolve(resource: uri, options: IResolveFileOptions = Object.create(null)): TPromise { + return this.toStatResolver(resource).then(model => model.resolve(options)); + } + + private toStatResolver(resource: uri): TPromise { + const absolutePath = this.toAbsolutePath(resource); + + return pfs.statLink(absolutePath).then(({ isSymbolicLink, stat }) => { + return new StatResolver(resource, isSymbolicLink, stat.isDirectory(), stat.mtime.getTime(), stat.size, this.environmentService.verbose ? err => this.handleError(err) : void 0); }); } public watchFileChanges(resource: uri): void { - if (!resource) { - return; + assert.ok(resource && resource.scheme === Schemas.file, `Invalid resource for watching: ${resource}`); + + // Create or get watcher for provided path + let watcher = this.activeFileChangesWatchers.get(resource); + if (!watcher) { + const fsPath = resource.fsPath; + const fsName = paths.basename(resource.fsPath); + + watcher = extfs.watch(fsPath, (eventType: string, filename: string) => { + const renamedOrDeleted = ((filename && filename !== fsName) || eventType === 'rename'); + + // The file was either deleted or renamed. Many tools apply changes to files in an + // atomic way ("Atomic Save") by first renaming the file to a temporary name and then + // renaming it back to the original name. Our watcher will detect this as a rename + // and then stops to work on Mac and Linux because the watcher is applied to the + // inode and not the name. The fix is to detect this case and trying to watch the file + // again after a certain delay. + // In addition, we send out a delete event if after a timeout we detect that the file + // does indeed not exist anymore. + if (renamedOrDeleted) { + + // Very important to dispose the watcher which now points to a stale inode + this.unwatchFileChanges(resource); + + // Wait a bit and try to install watcher again, assuming that the file was renamed quickly ("Atomic Save") + setTimeout(() => { + this.existsFile(resource).done(exists => { + + // File still exists, so reapply the watcher + if (exists) { + this.watchFileChanges(resource); + } + + // File seems to be really gone, so emit a deleted event + else { + this.onRawFileChange({ + type: FileChangeType.DELETED, + path: fsPath + }); + } + }); + }, FileService.FS_REWATCH_DELAY); + } + + // Handle raw file change + this.onRawFileChange({ + type: FileChangeType.UPDATED, + path: fsPath + }); + }, (error: string) => this.handleError(error)); + + if (watcher) { + this.activeFileChangesWatchers.set(resource, watcher); + } + } + } + + private onRawFileChange(event: IRawFileChange): void { + + // add to bucket of undelivered events + this.undeliveredRawFileChangesEvents.push(event); + + if (this.environmentService.verbose) { + console.log('%c[node.js Watcher]%c', 'color: green', 'color: black', event.type === FileChangeType.ADDED ? '[ADDED]' : event.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]', event.path); } - if (resource.scheme !== Schemas.file) { - return; // only support files - } + // handle emit through delayer to accommodate for bulk changes + this.fileChangesWatchDelayer.trigger(() => { + const buffer = this.undeliveredRawFileChangesEvents; + this.undeliveredRawFileChangesEvents = []; - // return early if the resource is inside the workspace for which we have another watcher in place - if (this.contextService.isInsideWorkspace(resource)) { - return; - } + // Normalize + const normalizedEvents = normalize(buffer); - this.raw.watchFileChanges(resource); + // Logging + if (this.environmentService.verbose) { + normalizedEvents.forEach(r => { + console.log('%c[node.js Watcher]%c >> normalized', 'color: green', 'color: black', r.type === FileChangeType.ADDED ? '[ADDED]' : r.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]', r.path); + }); + } + + // Emit + this._onFileChanges.fire(toFileChangesEvent(normalizedEvents)); + + return TPromise.as(null); + }); } public unwatchFileChanges(resource: uri): void { - this.raw.unwatchFileChanges(resource); - } - - public getEncoding(resource: uri, preferredEncoding?: string): string { - return this.raw.getEncoding(resource, preferredEncoding); + const watcher = this.activeFileChangesWatchers.get(resource); + if (watcher) { + watcher.close(); + this.activeFileChangesWatchers.delete(resource); + } } public dispose(): void { - this.toUnbind = dispose(this.toUnbind); + this.toDispose = dispose(this.toDispose); - // Dispose service - this.raw.dispose(); + if (this.activeWorkspaceFileChangeWatcher) { + this.activeWorkspaceFileChangeWatcher.dispose(); + this.activeWorkspaceFileChangeWatcher = null; + } + + this.activeFileChangesWatchers.forEach(watcher => watcher.close()); + this.activeFileChangesWatchers.clear(); + } +} + +function etag(stat: fs.Stats): string; +function etag(size: number, mtime: number): string; +function etag(arg1: any, arg2?: any): string { + let size: number; + let mtime: number; + if (typeof arg2 === 'number') { + size = arg1; + mtime = arg2; + } else { + size = (arg1).size; + mtime = (arg1).mtime.getTime(); + } + + return `"${crypto.createHash('sha1').update(String(size) + String(mtime)).digest('hex')}"`; +} + +export class StatResolver { + private name: string; + private etag: string; + + constructor( + private resource: uri, + private isSymbolicLink: boolean, + private isDirectory: boolean, + private mtime: number, + private size: number, + private errorLogger?: (error: Error | string) => void + ) { + assert.ok(resource && resource.scheme === Schemas.file, `Invalid resource: ${resource}`); + + this.name = getBaseLabel(resource); + this.etag = etag(size, mtime); + } + + public resolve(options: IResolveFileOptions): TPromise { + + // General Data + const fileStat: IFileStat = { + resource: this.resource, + isDirectory: this.isDirectory, + isSymbolicLink: this.isSymbolicLink, + name: this.name, + etag: this.etag, + size: this.size, + mtime: this.mtime + }; + + // File Specific Data + if (!this.isDirectory) { + return TPromise.as(fileStat); + } + + // Directory Specific Data + else { + + // Convert the paths from options.resolveTo to absolute paths + let absoluteTargetPaths: string[] = null; + if (options && options.resolveTo) { + absoluteTargetPaths = []; + options.resolveTo.forEach(resource => { + absoluteTargetPaths.push(resource.fsPath); + }); + } + + return new TPromise((c, e) => { + + // Load children + this.resolveChildren(this.resource.fsPath, absoluteTargetPaths, options && options.resolveSingleChildDescendants, children => { + children = arrays.coalesce(children); // we don't want those null children (could be permission denied when reading a child) + fileStat.children = children || []; + + c(fileStat); + }); + }); + } + } + + private resolveChildren(absolutePath: string, absoluteTargetPaths: string[], resolveSingleChildDescendants: boolean, callback: (children: IFileStat[]) => void): void { + extfs.readdir(absolutePath, (error: Error, files: string[]) => { + if (error) { + if (this.errorLogger) { + this.errorLogger(error); + } + + return callback(null); // return - we might not have permissions to read the folder + } + + // for each file in the folder + flow.parallel(files, (file: string, clb: (error: Error, children: IFileStat) => void) => { + const fileResource = uri.file(paths.resolve(absolutePath, file)); + let fileStat: fs.Stats; + let isSymbolicLink = false; + const $this = this; + + flow.sequence( + function onError(error: Error): void { + if ($this.errorLogger) { + $this.errorLogger(error); + } + + clb(null, null); // return - we might not have permissions to read the folder or stat the file + }, + + function stat(this: any): void { + extfs.statLink(fileResource.fsPath, this); + }, + + function countChildren(this: any, statAndLink: extfs.IStatAndLink): void { + fileStat = statAndLink.stat; + isSymbolicLink = statAndLink.isSymbolicLink; + + if (fileStat.isDirectory()) { + extfs.readdir(fileResource.fsPath, (error, result) => { + this(null, result ? result.length : 0); + }); + } else { + this(null, 0); + } + }, + + function resolve(childCount: number): void { + const childStat: IFileStat = { + resource: fileResource, + isDirectory: fileStat.isDirectory(), + isSymbolicLink, + name: file, + mtime: fileStat.mtime.getTime(), + etag: etag(fileStat), + size: fileStat.size + }; + + // Return early for files + if (!fileStat.isDirectory()) { + return clb(null, childStat); + } + + // Handle Folder + let resolveFolderChildren = false; + if (files.length === 1 && resolveSingleChildDescendants) { + resolveFolderChildren = true; + } else if (childCount > 0 && absoluteTargetPaths && absoluteTargetPaths.some(targetPath => isEqualOrParent(targetPath, fileResource.fsPath, !isLinux /* ignorecase */))) { + resolveFolderChildren = true; + } + + // Continue resolving children based on condition + if (resolveFolderChildren) { + $this.resolveChildren(fileResource.fsPath, absoluteTargetPaths, resolveSingleChildDescendants, children => { + children = arrays.coalesce(children); // we don't want those null children + childStat.children = children || []; + + clb(null, childStat); + }); + } + + // Otherwise return result + else { + clb(null, childStat); + } + }); + }, (errors, result) => { + callback(result); + }); + }); } } diff --git a/src/vs/workbench/services/files/electron-browser/remoteFileService.ts b/src/vs/workbench/services/files/electron-browser/remoteFileService.ts index 3f7fbe5d3f8..4cb7cde1d24 100644 --- a/src/vs/workbench/services/files/electron-browser/remoteFileService.ts +++ b/src/vs/workbench/services/files/electron-browser/remoteFileService.ts @@ -4,51 +4,49 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import URI from 'vs/base/common/uri'; -import { FileService } from 'vs/workbench/services/files/electron-browser/fileService'; -import { IContent, IStreamContent, IFileStat, IResolveContentOptions, IUpdateContentOptions, IResolveFileOptions, IResolveFileResult, FileOperationEvent, FileOperation, IFileSystemProvider, IStat, FileType, IImportResult, FileChangesEvent, ICreateFileOptions, FileOperationError, FileOperationResult, ITextSnapshot, snapshotToString } from 'vs/platform/files/common/files'; -import { TPromise } from 'vs/base/common/winjs.base'; import { posix } from 'path'; -import { IDisposable } from 'vs/base/common/lifecycle'; -import { isFalsyOrEmpty, distinct } from 'vs/base/common/arrays'; +import { flatten, isFalsyOrEmpty } from 'vs/base/common/arrays'; +import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { TernarySearchTree, keys } from 'vs/base/common/map'; import { Schemas } from 'vs/base/common/network'; -import { Progress } from 'vs/platform/progress/common/progress'; -import { decodeStream, encode, UTF8, UTF8_with_bom } from 'vs/base/node/encoding'; -import { TernarySearchTree } from 'vs/base/common/map'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; -import { IStorageService } from 'vs/platform/storage/common/storage'; +import URI from 'vs/base/common/uri'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { IDecodeStreamOptions, decodeStream, toDecodeStream } from 'vs/base/node/encoding'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/resourceConfiguration'; -import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { maxBufferLen, detectMimeAndEncodingFromBuffer } from 'vs/base/node/mime'; -import { MIME_BINARY } from 'vs/base/common/mime'; import { localize } from 'vs/nls'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { FileChangesEvent, FileOperation, FileOperationError, FileOperationEvent, FileOperationResult, FileOptions, FileSystemProviderCapabilities, IContent, ICreateFileOptions, IFileStat, IFileSystemProvider, IFilesConfiguration, IResolveContentOptions, IResolveFileOptions, IResolveFileResult, IStat, IStreamContent, ITextSnapshot, IUpdateContentOptions, StringSnapshot } from 'vs/platform/files/common/files'; +import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; import { INotificationService } from 'vs/platform/notification/common/notification'; +import { IStorageService } from 'vs/platform/storage/common/storage'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { FileService } from 'vs/workbench/services/files/electron-browser/fileService'; +import { createReadableOfProvider, createReadableOfSnapshot, createWritableOfProvider } from 'vs/workbench/services/files/electron-browser/streams'; function toIFileStat(provider: IFileSystemProvider, tuple: [URI, IStat], recurse?: (tuple: [URI, IStat]) => boolean): TPromise { const [resource, stat] = tuple; const fileStat: IFileStat = { - isDirectory: false, - isSymbolicLink: stat.type === FileType.Symlink, - resource: resource, + resource, name: posix.basename(resource.path), + isDirectory: stat.isDirectory, + isSymbolicLink: stat.isSymbolicLink, mtime: stat.mtime, size: stat.size, etag: stat.mtime.toString(29) + stat.size.toString(31), }; - if (stat.type === FileType.Dir) { - fileStat.isDirectory = true; - + if (fileStat.isDirectory) { if (recurse && recurse([resource, stat])) { // dir -> resolve return provider.readdir(resource).then(entries => { - fileStat.isDirectory = true; - // resolve children if requested - return TPromise.join(entries.map(stat => toIFileStat(provider, stat, recurse))).then(children => { + return TPromise.join(entries.map(tuple => { + const [name, stat] = tuple; + const childResource = resource.with({ path: posix.join(resource.path, name) }); + return toIFileStat(provider, [childResource, stat], recurse); + })).then(children => { fileStat.children = children; return fileStat; }); @@ -74,60 +72,169 @@ export function toDeepIFileStat(provider: IFileSystemProvider, tuple: [URI, ISta }); } +class WorkspaceWatchLogic { + + private _disposables: IDisposable[] = []; + private _watches = new Map(); + + constructor( + private _fileService: RemoteFileService, + @IConfigurationService private _configurationService: IConfigurationService, + @IWorkspaceContextService private _contextService: IWorkspaceContextService, + ) { + this._refresh(); + + this._disposables.push(this._contextService.onDidChangeWorkspaceFolders(e => { + for (const removed of e.removed) { + this._unwatchWorkspace(removed.uri); + } + for (const added of e.added) { + this._watchWorkspace(added.uri); + } + })); + this._disposables.push(this._contextService.onDidChangeWorkbenchState(e => { + this._refresh(); + })); + this._disposables.push(this._configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('files.watcherExclude')) { + this._refresh(); + } + })); + } + + dispose(): void { + this._unwatchWorkspaces(); + this._disposables = dispose(this._disposables); + } + + private _refresh(): void { + this._unwatchWorkspaces(); + for (const folder of this._contextService.getWorkspace().folders) { + if (folder.uri.scheme !== Schemas.file) { + this._watchWorkspace(folder.uri); + } + } + } + + private _watchWorkspace(resource: URI) { + let exclude: string[] = []; + let config = this._configurationService.getValue({ resource }); + if (config.files && config.files.watcherExclude) { + for (const key in config.files.watcherExclude) { + if (config.files.watcherExclude[key] === true) { + exclude.push(key); + } + } + } + this._watches.set(resource.toString(), resource); + this._fileService.watchFileChanges(resource, { recursive: true, exclude }); + } + + private _unwatchWorkspace(resource: URI) { + if (this._watches.has(resource.toString())) { + this._fileService.unwatchFileChanges(resource); + this._watches.delete(resource.toString()); + } + } + + private _unwatchWorkspaces() { + this._watches.forEach(uri => this._fileService.unwatchFileChanges(uri)); + this._watches.clear(); + } +} + export class RemoteFileService extends FileService { - private readonly _provider = new Map(); - private _supportedSchemes: string[]; + private readonly _provider: Map; + private readonly _lastKnownSchemes: string[]; constructor( @IExtensionService private readonly _extensionService: IExtensionService, @IStorageService private readonly _storageService: IStorageService, + @IEnvironmentService private readonly _environmentService: IEnvironmentService, @IConfigurationService configurationService: IConfigurationService, @IWorkspaceContextService contextService: IWorkspaceContextService, - @IEnvironmentService environmentService: IEnvironmentService, @ILifecycleService lifecycleService: ILifecycleService, @INotificationService notificationService: INotificationService, @ITextResourceConfigurationService textResourceConfigurationService: ITextResourceConfigurationService, ) { super( - configurationService, contextService, - environmentService, - lifecycleService, - notificationService, - _storageService, + _environmentService, textResourceConfigurationService, + configurationService, + lifecycleService, + _storageService, + notificationService ); - this._supportedSchemes = JSON.parse(this._storageService.get('remote_schemes', undefined, '[]')); + this._provider = new Map(); + this._lastKnownSchemes = JSON.parse(this._storageService.get('remote_schemes', undefined, '[]')); + this.toDispose.push(new WorkspaceWatchLogic(this, configurationService, contextService)); } - registerProvider(authority: string, provider: IFileSystemProvider): IDisposable { - if (this._provider.has(authority)) { - throw new Error(); + registerProvider(scheme: string, provider: IFileSystemProvider): IDisposable { + if (this._provider.has(scheme)) { + throw new Error('a provider for that scheme is already registered'); } - this._supportedSchemes.push(authority); - this._storageService.store('remote_schemes', JSON.stringify(distinct(this._supportedSchemes))); + this._provider.set(scheme, provider); + this._onDidChangeFileSystemProviderRegistrations.fire({ added: true, scheme, provider }); + this._storageService.store('remote_schemes', JSON.stringify(keys(this._provider))); - this._provider.set(authority, provider); - const reg = provider.onDidChange(changes => { + const reg = provider.onDidChangeFile(changes => { // forward change events this._onFileChanges.fire(new FileChangesEvent(changes)); }); return { dispose: () => { - this._provider.delete(authority); + this._onDidChangeFileSystemProviderRegistrations.fire({ added: false, scheme, provider }); + this._provider.delete(scheme); reg.dispose(); } }; } canHandleResource(resource: URI): boolean { - return resource.scheme === Schemas.file - || this._provider.has(resource.scheme) - // TODO@remote - || this._supportedSchemes.indexOf(resource.scheme) >= 0; + if (resource.scheme === Schemas.file || this._provider.has(resource.scheme)) { + return true; + } + // TODO@remote + // this needs to go, but this already went viral + // https://github.com/Microsoft/vscode/issues/48275 + if (this._lastKnownSchemes.indexOf(resource.scheme) < 0) { + return false; + } + if (!this._environmentService.isBuilt) { + console.warn('[remote] cache information required for ' + resource.toString()); + } + return true; + } + + private _tryParseFileOperationResult(err: any): FileOperationResult { + if (!(err instanceof Error)) { + return undefined; + } + let match = /^(.+) \(FileSystemError\)$/.exec(err.name); + if (!match) { + return undefined; + } + let res: FileOperationResult; + switch (match[1]) { + case 'EntryNotFound': + res = FileOperationResult.FILE_NOT_FOUND; + break; + case 'EntryIsADirectory': + res = FileOperationResult.FILE_IS_DIRECTORY; + break; + case 'EntryExists': + case 'EntryNotADirectory': + default: + // todo + res = undefined; + break; + } + return res; } // --- stat @@ -159,7 +266,10 @@ export class RemoteFileService extends FileService { } else { return this._doResolveFiles([{ resource, options }]).then(data => { if (data.length !== 1 || !data[0].success) { - throw new Error(`ENOENT, ${resource}`); + throw new FileOperationError( + localize('fileNotFoundError', "File not found ({0})", resource.toString(true)), + FileOperationResult.FILE_NOT_FOUND + ); } else { return data[0].stat; } @@ -188,9 +298,7 @@ export class RemoteFileService extends FileService { promises.push(this._doResolveFiles(group)); } } - return TPromise.join(promises).then(data => { - return [].concat(...data); - }); + return TPromise.join(promises).then(data => flatten(data)); } private _doResolveFiles(toResolve: { resource: URI; options?: IResolveFileOptions; }[]): TPromise { @@ -215,7 +323,7 @@ export class RemoteFileService extends FileService { if (resource.scheme === Schemas.file) { return super.resolveContent(resource, options); } else { - return this._doResolveContent(resource, options).then(RemoteFileService._asContent); + return this._readFile(resource, options).then(RemoteFileService._asContent); } } @@ -223,11 +331,11 @@ export class RemoteFileService extends FileService { if (resource.scheme === Schemas.file) { return super.resolveStreamContent(resource, options); } else { - return this._doResolveContent(resource, options); + return this._readFile(resource, options); } } - private _doResolveContent(resource: URI, options: IResolveContentOptions = Object.create(null)): TPromise { + private _readFile(resource: URI, options: IResolveContentOptions = Object.create(null)): TPromise { return this._withProvider(resource).then(provider => { return this.resolveFile(resource).then(fileStat => { @@ -249,20 +357,18 @@ export class RemoteFileService extends FileService { ); } - const guessEncoding = options.autoGuessEncoding; - const count = maxBufferLen(options); - const chunks: Buffer[] = []; + const decodeStreamOpts: IDecodeStreamOptions = { + guessEncoding: options.autoGuessEncoding, + overwriteEncoding: detected => { + return this.encoding.getReadEncoding(resource, options, { encoding: detected, seemsBinary: false }); + } + }; - return provider.read( - resource, - 0, count, - new Progress(chunk => chunks.push(chunk)) - ).then(bytesRead => { - // send to bla - return detectMimeAndEncodingFromBuffer({ bytesRead, buffer: Buffer.concat(chunks) }, guessEncoding); + const readable = createReadableOfProvider(provider, resource, options.position || 0, { read: true }); - }).then(detected => { - if (options.acceptTextOnly && detected.mimes.indexOf(MIME_BINARY) >= 0) { + return toDecodeStream(readable, decodeStreamOpts).then(data => { + + if (options.acceptTextOnly && data.detected.seemsBinary) { return TPromise.wrapError(new FileOperationError( localize('fileBinaryError', "File seems to be binary and cannot be opened as text"), FileOperationResult.FILE_IS_BINARY, @@ -270,53 +376,9 @@ export class RemoteFileService extends FileService { )); } - let preferredEncoding: string; - if (options && options.encoding) { - if (detected.encoding === UTF8 && options.encoding === UTF8) { - preferredEncoding = UTF8_with_bom; // indicate the file has BOM if we are to resolve with UTF 8 - } else { - preferredEncoding = options.encoding; // give passed in encoding highest priority - } - } else if (detected.encoding) { - if (detected.encoding === UTF8) { - preferredEncoding = UTF8_with_bom; // if we detected UTF-8, it can only be because of a BOM - } else { - preferredEncoding = detected.encoding; - } - // todo@remote - encoding logic should not be kept - // hostage inside the node file service - // } else if (super.configuredEncoding(resource) === UTF8_with_bom) { - } else { - preferredEncoding = UTF8; // if we did not detect UTF 8 BOM before, this can only be UTF 8 then - } - - // const encoding = this.getEncoding(resource); - const stream = decodeStream(preferredEncoding); - - // start with what we have already read - // and have a new stream to read the rest - let offset = 0; - for (const chunk of chunks) { - stream.write(chunk); - offset += chunk.length; - } - if (offset < count) { - // we didn't read enough the first time which means - // that we are done - stream.end(); - } else { - // there is more to read - provider.read(resource, offset, -1, new Progress(chunk => stream.write(chunk))).then(() => { - stream.end(); - }, err => { - stream.emit('error', err); - stream.end(); - }); - } - - return { - encoding: preferredEncoding, - value: stream, + return { + encoding: data.detected.encoding, + value: data.stream, resource: fileStat.resource, name: fileStat.name, etag: fileStat.etag, @@ -334,38 +396,45 @@ export class RemoteFileService extends FileService { return super.createFile(resource, content, options); } else { return this._withProvider(resource).then(provider => { - let prepare = options && !options.overwrite - ? this.existsFile(resource) - : TPromise.as(false); + const encoding = this.encoding.getWriteEncoding(resource); + return this._writeFile(provider, resource, new StringSnapshot(content), encoding, { write: true, create: true, exclusive: !(options && options.overwrite) }); - return prepare.then(exists => { - if (exists && options && !options.overwrite) { - return TPromise.wrapError(new FileOperationError('EEXIST', FileOperationResult.FILE_MODIFIED_SINCE, options)); - } - return this._doUpdateContent(provider, resource, content || '', {}); - }).then(fileStat => { - this._onAfterOperation.fire(new FileOperationEvent(resource, FileOperation.CREATE, fileStat)); - return fileStat; - }); + }).then(fileStat => { + this._onAfterOperation.fire(new FileOperationEvent(resource, FileOperation.CREATE, fileStat)); + return fileStat; + }, err => { + const message = localize('err.create', "Failed to create file {0}", resource.toString(false)); + const result = this._tryParseFileOperationResult(err); + throw new FileOperationError(message, result, options); }); } } - updateContent(resource: URI, value: string | ITextSnapshot, options?: IUpdateContentOptions): TPromise { + async updateContent(resource: URI, value: string | ITextSnapshot, options?: IUpdateContentOptions): TPromise { if (resource.scheme === Schemas.file) { return super.updateContent(resource, value, options); } else { + if (options && options.mkdirp) { + await this._mkdirp(resource.with({ path: posix.dirname(resource.path) })); + } return this._withProvider(resource).then(provider => { - return this._doUpdateContent(provider, resource, value, options || {}); + const snapshot = typeof value === 'string' ? new StringSnapshot(value) : value; + return this._writeFile(provider, resource, snapshot, options && options.encoding, { write: true }); }); } } - private _doUpdateContent(provider: IFileSystemProvider, resource: URI, content: string | ITextSnapshot, options: IUpdateContentOptions): TPromise { - const encoding = this.getEncoding(resource, options.encoding); - // TODO@Joh support streaming API for remote file system writes - return provider.write(resource, encode(typeof content === 'string' ? content : snapshotToString(content), encoding)).then(() => { + private _writeFile(provider: IFileSystemProvider, resource: URI, snapshot: ITextSnapshot, preferredEncoding: string, options: FileOptions): TPromise { + const readable = createReadableOfSnapshot(snapshot); + const encoding = this.encoding.getWriteEncoding(resource, preferredEncoding); + const decoder = decodeStream(encoding); + const target = createWritableOfProvider(provider, resource, options); + return new TPromise((resolve, reject) => { + readable.pipe(decoder).pipe(target); + target.once('error', err => reject(err)); + target.once('finish', _ => resolve(void 0)); + }).then(_ => { return this.resolveFile(resource); }); } @@ -386,6 +455,27 @@ export class RemoteFileService extends FileService { }); } + private async _mkdirp(directory: URI): Promise { + let basenames: string[] = []; + while (directory.path !== '/') { + try { + let stat = await this.resolveFile(directory); + if (!stat.isDirectory) { + throw new Error(`${directory.toString()} is not a directory`); + } + } catch (e) { + // ENOENT + basenames.push(posix.basename(directory.path)); + directory = directory.with({ path: posix.dirname(directory.path) }); + } + break; + } + for (let i = basenames.length - 1; i >= 0; i--) { + directory = directory.with({ path: posix.join(directory.path, basenames[i]) }); + await this.createFolder(directory); + } + } + // --- delete del(resource: URI, useTrash?: boolean): TPromise { @@ -393,9 +483,7 @@ export class RemoteFileService extends FileService { return super.del(resource, useTrash); } else { return this._withProvider(resource).then(provider => { - return provider.stat(resource).then(stat => { - return stat.type === FileType.Dir ? provider.rmdir(resource) : provider.unlink(resource); - }).then(() => { + return provider.delete(resource).then(() => { this._onAfterOperation.fire(new FileOperationEvent(resource, FileOperation.DELETE)); }); }); @@ -443,7 +531,7 @@ export class RemoteFileService extends FileService { : TPromise.as(null); return prepare.then(() => this._withProvider(source)).then(provider => { - return provider.move(source, target).then(stat => { + return provider.rename(source, target, { create: true, exclusive: !overwrite }).then(stat => { return toIFileStat(provider, [target, stat]); }).then(fileStat => { this._onAfterOperation.fire(new FileOperationEvent(source, FileOperation.MOVE, fileStat)); @@ -463,75 +551,81 @@ export class RemoteFileService extends FileService { }); } - importFile(source: URI, targetFolder: URI): TPromise { - if (source.scheme === targetFolder.scheme && source.scheme === Schemas.file) { - return super.importFile(source, targetFolder); - } else { - const target = targetFolder.with({ path: posix.join(targetFolder.path, posix.basename(source.path)) }); - return this.copyFile(source, target, false).then(stat => ({ stat, isNew: false })); - } - } - copyFile(source: URI, target: URI, overwrite?: boolean): TPromise { if (source.scheme === target.scheme && source.scheme === Schemas.file) { return super.copyFile(source, target, overwrite); } - const prepare = overwrite - ? this.del(target).then(undefined, err => { /*ignore*/ }) - : TPromise.as(null); + return this._withProvider(target).then(provider => { - return prepare.then(() => { - // todo@ben, can only copy text files - // https://github.com/Microsoft/vscode/issues/41543 - return this.resolveContent(source, { acceptTextOnly: true }).then(content => { - return this._withProvider(target).then(provider => { - return this._doUpdateContent(provider, target, content.value, { encoding: content.encoding }).then(fileStat => { - this._onAfterOperation.fire(new FileOperationEvent(source, FileOperation.COPY, fileStat)); - return fileStat; + if (source.scheme === target.scheme && (provider.capabilities & FileSystemProviderCapabilities.FileFolderCopy)) { + // good: provider supports copy withing scheme + return provider.copy(source, target, { create: true, exclusive: !overwrite }).then(stat => toIFileStat(provider, [target, stat])); + } + + const prepare = overwrite + ? this.del(target).then(undefined, err => { /*ignore*/ }) + : TPromise.as(null); + + return prepare.then(() => { + // todo@ben, can only copy text files + // https://github.com/Microsoft/vscode/issues/41543 + return this.resolveContent(source, { acceptTextOnly: true }).then(content => { + return this._withProvider(target).then(provider => { + return this._writeFile( + provider, target, + new StringSnapshot(content.value), + content.encoding, + { write: true, create: true, exclusive: !overwrite } + ).then(fileStat => { + this._onAfterOperation.fire(new FileOperationEvent(source, FileOperation.COPY, fileStat)); + return fileStat; + }); + }, err => { + if (err instanceof Error && err.name === 'ENOPRO') { + // file scheme + return super.updateContent(target, content.value, { encoding: content.encoding }); + } else { + return TPromise.wrapError(err); + } }); - }, err => { - if (err instanceof Error && err.name === 'ENOPRO') { - // file scheme - return super.updateContent(target, content.value, { encoding: content.encoding }); - } else { - return TPromise.wrapError(err); - } }); }); }); - } - touchFile(resource: URI): TPromise { + private _activeWatches = new Map, count: number }>(); + + public watchFileChanges(resource: URI, opts: { recursive?: boolean, exclude?: string[] } = {}): void { if (resource.scheme === Schemas.file) { - return super.touchFile(resource); - } else { - return this._doTouchFile(resource); + return super.watchFileChanges(resource); } - } - private _doTouchFile(resource: URI): TPromise { - return this._withProvider(resource).then(provider => { - return provider.stat(resource).then(() => { - return provider.utimes(resource, Date.now(), Date.now()); + const key = resource.toString(); + const entry = this._activeWatches.get(key); + if (entry) { + entry.count += 1; + return; + } + + this._activeWatches.set(key, { + count: 1, + unwatch: this._withProvider(resource).then(provider => { + return provider.watch(resource, opts); }, err => { - return provider.write(resource, new Uint8Array(0)); - }).then(() => { - return this.resolveFile(resource); - }); + return { dispose() { } }; + }) }); } - // TODO@Joh - file watching on demand! - public watchFileChanges(resource: URI): void { - if (resource.scheme === Schemas.file) { - super.watchFileChanges(resource); - } - } public unwatchFileChanges(resource: URI): void { if (resource.scheme === Schemas.file) { - super.unwatchFileChanges(resource); + return super.unwatchFileChanges(resource); + } + let entry = this._activeWatches.get(resource.toString()); + if (entry && --entry.count === 0) { + entry.unwatch.then(dispose); + this._activeWatches.delete(resource.toString()); } } } diff --git a/src/vs/workbench/services/files/electron-browser/streams.ts b/src/vs/workbench/services/files/electron-browser/streams.ts new file mode 100644 index 00000000000..583166be2cc --- /dev/null +++ b/src/vs/workbench/services/files/electron-browser/streams.ts @@ -0,0 +1,168 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { Readable, Writable } from 'stream'; +import { UTF8 } from 'vs/base/node/encoding'; +import URI from 'vs/base/common/uri'; +import { IFileSystemProvider, ITextSnapshot, FileSystemProviderCapabilities, FileOptions } from 'vs/platform/files/common/files'; +import { illegalArgument } from 'vs/base/common/errors'; + +export function createWritableOfProvider(provider: IFileSystemProvider, resource: URI, opts: FileOptions): Writable { + if (provider.capabilities & FileSystemProviderCapabilities.FileOpenReadWriteClose) { + return createWritable(provider, resource, opts); + } else if (provider.capabilities & FileSystemProviderCapabilities.FileReadWrite) { + return createSimpleWritable(provider, resource, opts); + } else { + throw illegalArgument(); + } +} + +function createSimpleWritable(provider: IFileSystemProvider, resource: URI, opts: FileOptions): Writable { + return new class extends Writable { + _chunks: Buffer[] = []; + constructor(opts?) { + super(opts); + } + _write(chunk: Buffer, encoding: string, callback: Function) { + this._chunks.push(chunk); + callback(null); + } + end() { + // todo@joh - end might have another chunk... + provider.writeFile(resource, Buffer.concat(this._chunks), opts).then(_ => { + super.end(); + }, err => { + this.emit('error', err); + }); + } + }; +} + +function createWritable(provider: IFileSystemProvider, resource: URI, opts: FileOptions): Writable { + return new class extends Writable { + _fd: number; + _pos: number; + constructor(opts?) { + super(opts); + } + async _write(chunk: Buffer, encoding, callback: Function) { + try { + if (typeof this._fd !== 'number') { + this._fd = await provider.open(resource, opts); + } + let bytesWritten = await provider.write(this._fd, this._pos, chunk, 0, chunk.length); + this._pos += bytesWritten; + callback(); + } catch (err) { + callback(err); + } + } + end() { + provider.close(this._fd).then(_ => { + super.end(); + }, err => { + this.emit('error', err); + }); + } + }; +} + +export function createReadableOfProvider(provider: IFileSystemProvider, resource: URI, position: number, opts: FileOptions): Readable { + if (provider.capabilities & FileSystemProviderCapabilities.FileOpenReadWriteClose) { + return createReadable(provider, resource, position, opts); + } else if (provider.capabilities & FileSystemProviderCapabilities.FileReadWrite) { + return createSimpleReadable(provider, resource, position, opts); + } else { + throw illegalArgument(); + } +} + +function createReadable(provider: IFileSystemProvider, resource: URI, position: number, opts: FileOptions): Readable { + return new class extends Readable { + _fd: number; + _pos: number = position; + _reading: boolean = false; + constructor(opts?) { + super(opts); + this.once('close', _ => this._final()); + } + async _read(size?: number) { + if (this._reading) { + return; + } + this._reading = true; + try { + if (typeof this._fd !== 'number') { + this._fd = await provider.open(resource, opts); + } + let buffer = Buffer.allocUnsafe(64 * 1024); + while (this._reading) { + let bytesRead = await provider.read(this._fd, this._pos, buffer, 0, buffer.length); + if (bytesRead === 0) { + this._reading = false; + this.push(null); + } + else { + this._reading = this.push(buffer.slice(0, bytesRead)); + this._pos += bytesRead; + } + } + } + catch (err) { + // + this.emit('error', err); + } + } + async _final() { + if (typeof this._fd === 'number') { + await provider.close(this._fd); + } + } + }; +} + +function createSimpleReadable(provider: IFileSystemProvider, resource: URI, position: number, opts: FileOptions): Readable { + return new class extends Readable { + _readOperation: Thenable; + _read(size?: number): void { + if (this._readOperation) { + return; + } + this._readOperation = provider.readFile(resource, opts).then(data => { + this.push(data.slice(position)); + this.push(null); + }, err => { + this.emit('error', err); + this.push(null); + }); + } + }; +} + +export function createReadableOfSnapshot(snapshot: ITextSnapshot): Readable { + return new Readable({ + read: function () { + try { + let chunk: string; + let canPush = true; + + // Push all chunks as long as we can push and as long as + // the underlying snapshot returns strings to us + while (canPush && typeof (chunk = snapshot.read()) === 'string') { + canPush = this.push(chunk); + } + + // Signal EOS by pushing NULL + if (typeof chunk !== 'string') { + this.push(null); + } + } catch (error) { + this.emit('error', error); + } + }, + encoding: UTF8 // very important, so that strings are passed around and not buffers! + }); +} diff --git a/src/vs/workbench/services/files/node/fileService.ts b/src/vs/workbench/services/files/node/fileService.ts deleted file mode 100644 index 873c0914b1e..00000000000 --- a/src/vs/workbench/services/files/node/fileService.ts +++ /dev/null @@ -1,1305 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; - -import * as paths from 'path'; -import * as fs from 'fs'; -import * as os from 'os'; -import * as crypto from 'crypto'; -import * as assert from 'assert'; -import { isParent, FileOperation, FileOperationEvent, IContent, IFileService, IResolveFileOptions, IResolveFileResult, IResolveContentOptions, IFileStat, IStreamContent, FileOperationError, FileOperationResult, IUpdateContentOptions, FileChangeType, IImportResult, FileChangesEvent, ICreateFileOptions, IContentData, ITextSnapshot } from 'vs/platform/files/common/files'; -import { MAX_FILE_SIZE, MAX_HEAP_SIZE } from 'vs/platform/files/node/files'; -import { isEqualOrParent } from 'vs/base/common/paths'; -import { ResourceMap } from 'vs/base/common/map'; -import * as arrays from 'vs/base/common/arrays'; -import * as baseMime from 'vs/base/common/mime'; -import { TPromise } from 'vs/base/common/winjs.base'; -import * as objects from 'vs/base/common/objects'; -import * as extfs from 'vs/base/node/extfs'; -import { nfcall, ThrottledDelayer } from 'vs/base/common/async'; -import uri from 'vs/base/common/uri'; -import * as nls from 'vs/nls'; -import { isWindows, isLinux } from 'vs/base/common/platform'; -import { dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; -import * as pfs from 'vs/base/node/pfs'; -import * as encoding from 'vs/base/node/encoding'; -import { detectMimeAndEncodingFromBuffer, IMimeAndEncoding } from 'vs/base/node/mime'; -import * as flow from 'vs/base/node/flow'; -import { FileWatcher as UnixWatcherService } from 'vs/workbench/services/files/node/watcher/unix/watcherService'; -import { FileWatcher as WindowsWatcherService } from 'vs/workbench/services/files/node/watcher/win32/watcherService'; -import { toFileChangesEvent, normalize, IRawFileChange } from 'vs/workbench/services/files/node/watcher/common'; -import { Event, Emitter } from 'vs/base/common/event'; -import { FileWatcher as NsfwWatcherService } from 'vs/workbench/services/files/node/watcher/nsfw/watcherService'; -import { ITextResourceConfigurationService } from 'vs/editor/common/services/resourceConfiguration'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; -import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; -import { getBaseLabel } from 'vs/base/common/labels'; -import { Readable } from 'stream'; -import { Schemas } from 'vs/base/common/network'; - -export interface IEncodingOverride { - resource: uri; - encoding: string; -} - -export interface IFileServiceOptions { - tmpDir?: string; - errorLogger?: (msg: string) => void; - encodingOverride?: IEncodingOverride[]; - watcherIgnoredPatterns?: string[]; - disableWatcher?: boolean; - verboseLogging?: boolean; - useExperimentalFileWatcher?: boolean; - writeElevated?: (source: string, target: string) => TPromise; - elevationSupport?: { - cliPath: string; - promptTitle: string; - promptIcnsPath?: string; - }; -} - -function etag(stat: fs.Stats): string; -function etag(size: number, mtime: number): string; -function etag(arg1: any, arg2?: any): string { - let size: number; - let mtime: number; - if (typeof arg2 === 'number') { - size = arg1; - mtime = arg2; - } else { - size = (arg1).size; - mtime = (arg1).mtime.getTime(); - } - - return `"${crypto.createHash('sha1').update(String(size) + String(mtime)).digest('hex')}"`; -} - -class BufferPool { - - static _64K = new BufferPool(64 * 1024, 5); - - constructor( - readonly bufferSize: number, - private readonly _capacity: number, - private readonly _free: Buffer[] = [], - ) { - // - } - - acquire(): Buffer { - if (this._free.length === 0) { - return Buffer.allocUnsafe(this.bufferSize); - } else { - return this._free.shift(); - } - } - - release(buf: Buffer): void { - if (this._free.length <= this._capacity) { - this._free.push(buf); - } - } -} - -export class FileService implements IFileService { - - public _serviceBrand: any; - - private static readonly FS_EVENT_DELAY = 50; // aggregate and only emit events when changes have stopped for this duration (in ms) - private static readonly FS_REWATCH_DELAY = 300; // delay to rewatch a file that was renamed or deleted (in ms) - - private tmpPath: string; - private options: IFileServiceOptions; - - private readonly _onFileChanges: Emitter; - private readonly _onAfterOperation: Emitter; - - private toDispose: IDisposable[]; - - private activeFileChangesWatchers: ResourceMap; - private fileChangesWatchDelayer: ThrottledDelayer; - private undeliveredRawFileChangesEvents: IRawFileChange[]; - - private activeWorkspaceFileChangeWatcher: IDisposable; - - constructor( - private contextService: IWorkspaceContextService, - private environmentService: IEnvironmentService, - private textResourceConfigurationService: ITextResourceConfigurationService, - private configurationService: IConfigurationService, - private lifecycleService: ILifecycleService, - options: IFileServiceOptions - ) { - this.toDispose = []; - this.options = options || Object.create(null); - this.tmpPath = this.options.tmpDir || os.tmpdir(); - - this._onFileChanges = new Emitter(); - this.toDispose.push(this._onFileChanges); - - this._onAfterOperation = new Emitter(); - this.toDispose.push(this._onAfterOperation); - - if (!this.options.errorLogger) { - this.options.errorLogger = console.error; - } - - this.activeFileChangesWatchers = new ResourceMap(); - this.fileChangesWatchDelayer = new ThrottledDelayer(FileService.FS_EVENT_DELAY); - this.undeliveredRawFileChangesEvents = []; - - lifecycleService.when(LifecyclePhase.Running).then(() => { - this.setupFileWatching(); // wait until we are fully running before starting file watchers - }); - - this.registerListeners(); - } - - private registerListeners(): void { - this.toDispose.push(this.contextService.onDidChangeWorkbenchState(() => { - if (this.lifecycleService.phase >= LifecyclePhase.Running) { - this.setupFileWatching(); - } - })); - } - - public get onFileChanges(): Event { - return this._onFileChanges.event; - } - - public get onAfterOperation(): Event { - return this._onAfterOperation.event; - } - - public updateOptions(options: IFileServiceOptions): void { - if (options) { - objects.mixin(this.options, options); // overwrite current options - } - } - - private setupFileWatching(): void { - - // dispose old if any - if (this.activeWorkspaceFileChangeWatcher) { - this.activeWorkspaceFileChangeWatcher.dispose(); - } - - // Return if not aplicable - const workbenchState = this.contextService.getWorkbenchState(); - if (workbenchState === WorkbenchState.EMPTY || this.options.disableWatcher) { - return; - } - - // new watcher: use it if setting tells us so or we run in multi-root environment - if (this.options.useExperimentalFileWatcher || workbenchState === WorkbenchState.WORKSPACE) { - this.activeWorkspaceFileChangeWatcher = toDisposable(this.setupNsfwWorkspaceWatching().startWatching()); - } - - // old watcher - else { - if (isWindows) { - this.activeWorkspaceFileChangeWatcher = toDisposable(this.setupWin32WorkspaceWatching().startWatching()); - } else { - this.activeWorkspaceFileChangeWatcher = toDisposable(this.setupUnixWorkspaceWatching().startWatching()); - } - } - } - - private setupWin32WorkspaceWatching(): WindowsWatcherService { - return new WindowsWatcherService(this.contextService, this.options.watcherIgnoredPatterns, e => this._onFileChanges.fire(e), this.options.errorLogger, this.options.verboseLogging); - } - - private setupUnixWorkspaceWatching(): UnixWatcherService { - return new UnixWatcherService(this.contextService, this.options.watcherIgnoredPatterns, e => this._onFileChanges.fire(e), this.options.errorLogger, this.options.verboseLogging); - } - - private setupNsfwWorkspaceWatching(): NsfwWatcherService { - return new NsfwWatcherService(this.contextService, this.configurationService, e => this._onFileChanges.fire(e), this.options.errorLogger, this.options.verboseLogging); - } - - public resolveFile(resource: uri, options?: IResolveFileOptions): TPromise { - return this.resolve(resource, options); - } - - public resolveFiles(toResolve: { resource: uri, options?: IResolveFileOptions }[]): TPromise { - return TPromise.join(toResolve.map(resourceAndOptions => this.resolve(resourceAndOptions.resource, resourceAndOptions.options) - .then(stat => ({ stat, success: true }), error => ({ stat: void 0, success: false })))); - } - - public existsFile(resource: uri): TPromise { - return this.resolveFile(resource).then(() => true, () => false); - } - - public resolveContent(resource: uri, options?: IResolveContentOptions): TPromise { - return this.resolveStreamContent(resource, options).then(streamContent => { - return new TPromise((resolve, reject) => { - - const result: IContent = { - resource: streamContent.resource, - name: streamContent.name, - mtime: streamContent.mtime, - etag: streamContent.etag, - encoding: streamContent.encoding, - value: '' - }; - - streamContent.value.on('data', chunk => result.value += chunk); - streamContent.value.on('error', err => reject(err)); - streamContent.value.on('end', _ => resolve(result)); - - return result; - }); - }); - } - - public resolveStreamContent(resource: uri, options?: IResolveContentOptions): TPromise { - - // Guard early against attempts to resolve an invalid file path - if (resource.scheme !== Schemas.file || !resource.fsPath) { - return TPromise.wrapError(new FileOperationError( - nls.localize('fileInvalidPath', "Invalid file resource ({0})", resource.toString(true)), - FileOperationResult.FILE_INVALID_PATH, - options - )); - } - - const result: IStreamContent = { - resource: void 0, - name: void 0, - mtime: void 0, - etag: void 0, - encoding: void 0, - value: void 0 - }; - - const contentResolverTokenSource = new CancellationTokenSource(); - - const onStatError = (error: Error) => { - - // error: stop reading the file the stat and content resolve call - // usually race, mostly likely the stat call will win and cancel - // the content call - contentResolverTokenSource.cancel(); - - // forward error - return TPromise.wrapError(error); - }; - - const statsPromise = this.resolveFile(resource).then(stat => { - result.resource = stat.resource; - result.name = stat.name; - result.mtime = stat.mtime; - result.etag = stat.etag; - - // Return early if resource is a directory - if (stat.isDirectory) { - return onStatError(new FileOperationError( - nls.localize('fileIsDirectoryError', "File is directory"), - FileOperationResult.FILE_IS_DIRECTORY, - options - )); - } - - // Return early if file not modified since - if (options && options.etag && options.etag === stat.etag) { - return onStatError(new FileOperationError( - nls.localize('fileNotModifiedError', "File not modified since"), - FileOperationResult.FILE_NOT_MODIFIED_SINCE, - options - )); - } - - // Return early if file is too large to load - if (typeof stat.size === 'number') { - if (stat.size > Math.max(this.environmentService.args['max-memory'] * 1024 * 1024 || 0, MAX_HEAP_SIZE)) { - let memoryLimit = this.textResourceConfigurationService.getValue(null, 'files.maxMemoryForLargeFilesMB') | 4096; - return onStatError(new FileOperationError( - nls.localize('fileTooLargeForHeapError', "File size exceeds the default memory limit. Relaunch with a higher limit. The current setting is configured to relaunch with {0}MB", memoryLimit), - FileOperationResult.FILE_EXCEED_MEMORY_LIMIT - )); - } - - if (stat.size > MAX_FILE_SIZE) { - return onStatError(new FileOperationError( - nls.localize('fileTooLargeError', "File too large to open"), - FileOperationResult.FILE_TOO_LARGE - )); - } - } - - return void 0; - }, err => { - - // Wrap file not found errors - if (err.code === 'ENOENT') { - return onStatError(new FileOperationError( - nls.localize('fileNotFoundError', "File not found ({0})", resource.toString(true)), - FileOperationResult.FILE_NOT_FOUND, - options - )); - } - - return onStatError(err); - }); - - let completePromise: Thenable; - - // await the stat iff we already have an etag so that we compare the - // etag from the stat before we actually read the file again. - if (options && options.etag) { - completePromise = statsPromise.then(() => { - return this.fillInContents(result, resource, options, contentResolverTokenSource.token); // Waterfall -> only now resolve the contents - }); - } - - // a fresh load without a previous etag which means we can resolve the file stat - // and the content at the same time, avoiding the waterfall. - else { - completePromise = Promise.all([statsPromise, this.fillInContents(result, resource, options, contentResolverTokenSource.token)]); - } - - return TPromise.wrap(completePromise).then(() => { - contentResolverTokenSource.dispose(); - - return result; - }); - } - - private fillInContents(content: IStreamContent, resource: uri, options: IResolveContentOptions, token: CancellationToken): Thenable { - return this.resolveFileData(resource, options, token).then(data => { - content.encoding = data.encoding; - content.value = data.stream; - }); - } - - private resolveFileData(resource: uri, options: IResolveContentOptions, token: CancellationToken): Thenable { - - const chunkBuffer = BufferPool._64K.acquire(); - - const result: IContentData = { - encoding: void 0, - stream: void 0 - }; - - return new Promise((resolve, reject) => { - fs.open(this.toAbsolutePath(resource), 'r', (err, fd) => { - if (err) { - if (err.code === 'ENOENT') { - // Wrap file not found errors - err = new FileOperationError( - nls.localize('fileNotFoundError', "File not found ({0})", resource.toString(true)), - FileOperationResult.FILE_NOT_FOUND, - options - ); - } - - return reject(err); - } - - let decoder: NodeJS.ReadWriteStream; - let totalBytesRead = 0; - - const finish = (err?: any) => { - - if (err) { - if (err.code === 'EISDIR') { - // Wrap EISDIR errors (fs.open on a directory works, but you cannot read from it) - err = new FileOperationError( - nls.localize('fileIsDirectoryError', "File is directory"), - FileOperationResult.FILE_IS_DIRECTORY, - options - ); - } - if (decoder) { - // If the decoder already started, we have to emit the error through it as - // event because the promise is already resolved! - decoder.emit('error', err); - } else { - reject(err); - } - } - if (decoder) { - decoder.end(); - } - - // return the shared buffer - BufferPool._64K.release(chunkBuffer); - - if (fd) { - fs.close(fd, err => { - if (err) { - this.options.errorLogger(`resolveFileData#close(): ${err.toString()}`); - } - }); - } - }; - - const handleChunk = (bytesRead: number) => { - if (token.isCancellationRequested) { - // cancellation -> finish - finish(new Error('cancelled')); - } else if (bytesRead === 0) { - // no more data -> finish - finish(); - } else if (bytesRead < chunkBuffer.length) { - // write the sub-part of data we received -> repeat - decoder.write(chunkBuffer.slice(0, bytesRead), readChunk); - } else { - // write all data we received -> repeat - decoder.write(chunkBuffer, readChunk); - } - }; - - let currentPosition: number = (options && options.position) || null; - - const readChunk = () => { - fs.read(fd, chunkBuffer, 0, chunkBuffer.length, currentPosition, (err, bytesRead) => { - totalBytesRead += bytesRead; - - if (typeof currentPosition === 'number') { - // if we received a position argument as option we need to ensure that - // we advance the position by the number of bytesread - currentPosition += bytesRead; - } - - if (totalBytesRead > Math.max(this.environmentService.args['max-memory'] * 1024 * 1024 || 0, MAX_HEAP_SIZE)) { - let memoryLimit = this.textResourceConfigurationService.getValue(null, 'files.maxMemoryForLargeFilesMB') | 4096; - finish(new FileOperationError( - nls.localize('fileTooLargeForHeapError', "File size exceeds the default memory limit. Relaunch with a higher limit. The current setting is configured to relaunch with {0}MB", memoryLimit), - FileOperationResult.FILE_EXCEED_MEMORY_LIMIT - )); - } - - if (totalBytesRead > MAX_FILE_SIZE) { - // stop when reading too much - finish(new FileOperationError( - nls.localize('fileTooLargeError', "File too large to open"), - FileOperationResult.FILE_TOO_LARGE, - options - )); - } else if (err) { - // some error happened - finish(err); - - } else if (decoder) { - // pass on to decoder - handleChunk(bytesRead); - - } else { - // when receiving the first chunk of data we need to create the - // decoding stream which is then used to drive the string stream. - TPromise.as(detectMimeAndEncodingFromBuffer( - { buffer: chunkBuffer, bytesRead }, - options && options.autoGuessEncoding || this.configuredAutoGuessEncoding(resource) - )).then(value => { - - if (options && options.acceptTextOnly && value.mimes.indexOf(baseMime.MIME_BINARY) >= 0) { - // Return error early if client only accepts text and this is not text - finish(new FileOperationError( - nls.localize('fileBinaryError', "File seems to be binary and cannot be opened as text"), - FileOperationResult.FILE_IS_BINARY, - options - )); - - } else { - result.encoding = this.getEncoding(resource, this.getPeferredEncoding(resource, options, value)); - result.stream = decoder = encoding.decodeStream(result.encoding); - resolve(result); - handleChunk(bytesRead); - } - - }).then(void 0, err => { - // failed to get encoding - finish(err); - }); - } - }); - }; - - // start reading - readChunk(); - }); - }); - } - - public updateContent(resource: uri, value: string | ITextSnapshot, options: IUpdateContentOptions = Object.create(null)): TPromise { - if (this.options.elevationSupport && options.writeElevated) { - return this.doUpdateContentElevated(resource, value, options); - } - - return this.doUpdateContent(resource, value, options); - } - - private doUpdateContent(resource: uri, value: string | ITextSnapshot, options: IUpdateContentOptions = Object.create(null)): TPromise { - const absolutePath = this.toAbsolutePath(resource); - - // 1.) check file - return this.checkFile(absolutePath, options).then(exists => { - let createParentsPromise: TPromise; - if (exists) { - createParentsPromise = TPromise.as(null); - } else { - createParentsPromise = pfs.mkdirp(paths.dirname(absolutePath)); - } - - // 2.) create parents as needed - return createParentsPromise.then(() => { - const encodingToWrite = this.getEncoding(resource, options.encoding); - let addBomPromise: TPromise = TPromise.as(false); - - // UTF_16 BE and LE as well as UTF_8 with BOM always have a BOM - if (encodingToWrite === encoding.UTF16be || encodingToWrite === encoding.UTF16le || encodingToWrite === encoding.UTF8_with_bom) { - addBomPromise = TPromise.as(true); - } - - // Existing UTF-8 file: check for options regarding BOM - else if (exists && encodingToWrite === encoding.UTF8) { - if (options.overwriteEncoding) { - addBomPromise = TPromise.as(false); // if we are to overwrite the encoding, we do not preserve it if found - } else { - addBomPromise = encoding.detectEncodingByBOM(absolutePath).then(enc => enc === encoding.UTF8); // otherwise preserve it if found - } - } - - // 3.) check to add UTF BOM - return addBomPromise.then(addBom => { - - // 4.) set contents and resolve - return this.doSetContentsAndResolve(resource, absolutePath, value, addBom, encodingToWrite).then(void 0, error => { - if (!exists || error.code !== 'EPERM' || !isWindows) { - return TPromise.wrapError(error); - } - - // On Windows and if the file exists with an EPERM error, we try a different strategy of saving the file - // by first truncating the file and then writing with r+ mode. This helps to save hidden files on Windows - // (see https://github.com/Microsoft/vscode/issues/931) - - // 5.) truncate - return pfs.truncate(absolutePath, 0).then(() => { - - // 6.) set contents (this time with r+ mode) and resolve again - return this.doSetContentsAndResolve(resource, absolutePath, value, addBom, encodingToWrite, { flag: 'r+' }); - }); - }); - }); - }); - }).then(null, error => { - if (error.code === 'EACCES' || error.code === 'EPERM') { - return TPromise.wrapError(new FileOperationError( - nls.localize('filePermission', "Permission denied writing to file ({0})", resource.toString(true)), - FileOperationResult.FILE_PERMISSION_DENIED, - options - )); - } - - return TPromise.wrapError(error); - }); - } - - private doSetContentsAndResolve(resource: uri, absolutePath: string, value: string | ITextSnapshot, addBOM: boolean, encodingToWrite: string, options?: { mode?: number; flag?: string; }): TPromise { - let writeFilePromise: TPromise; - - // Configure encoding related options as needed - const writeFileOptions: extfs.IWriteFileOptions = options ? options : Object.create(null); - if (addBOM || encodingToWrite !== encoding.UTF8) { - writeFileOptions.encoding = { - charset: encodingToWrite, - addBOM - }; - } - - if (typeof value === 'string') { - writeFilePromise = pfs.writeFile(absolutePath, value, writeFileOptions); - } else { - writeFilePromise = pfs.writeFile(absolutePath, this.snapshotToReadableStream(value), writeFileOptions); - } - - // set contents - return writeFilePromise.then(() => { - - // resolve - return this.resolve(resource); - }); - } - - private snapshotToReadableStream(snapshot: ITextSnapshot): NodeJS.ReadableStream { - return new Readable({ - read: function () { - try { - let chunk: string; - let canPush = true; - - // Push all chunks as long as we can push and as long as - // the underlying snapshot returns strings to us - while (canPush && typeof (chunk = snapshot.read()) === 'string') { - canPush = this.push(chunk); - } - - // Signal EOS by pushing NULL - if (typeof chunk !== 'string') { - this.push(null); - } - } catch (error) { - this.emit('error', error); - } - }, - encoding: encoding.UTF8 // very important, so that strings are passed around and not buffers! - }); - } - - private doUpdateContentElevated(resource: uri, value: string | ITextSnapshot, options: IUpdateContentOptions = Object.create(null)): TPromise { - const absolutePath = this.toAbsolutePath(resource); - - // 1.) check file - return this.checkFile(absolutePath, options, options.overwriteReadonly /* ignore readonly if we overwrite readonly, this is handled via sudo later */).then(exists => { - const writeOptions: IUpdateContentOptions = objects.assign(Object.create(null), options); - writeOptions.writeElevated = false; - writeOptions.encoding = this.getEncoding(resource, options.encoding); - - // 2.) write to a temporary file to be able to copy over later - const tmpPath = paths.join(this.tmpPath, `code-elevated-${Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 6)}`); - return this.updateContent(uri.file(tmpPath), value, writeOptions).then(() => { - - // 3.) invoke our CLI as super user - return (import('sudo-prompt')).then(sudoPrompt => { - return new TPromise((c, e) => { - const promptOptions = { name: this.options.elevationSupport.promptTitle.replace('-', ''), icns: this.options.elevationSupport.promptIcnsPath }; - - const sudoCommand: string[] = [`"${this.options.elevationSupport.cliPath}"`]; - if (options.overwriteReadonly) { - sudoCommand.push('--file-chmod'); - } - sudoCommand.push('--file-write', `"${tmpPath}"`, `"${absolutePath}"`); - - sudoPrompt.exec(sudoCommand.join(' '), promptOptions, (error: string, stdout: string, stderr: string) => { - if (error || stderr) { - e(error || stderr); - } else { - c(void 0); - } - }); - }); - }).then(() => { - - // 3.) delete temp file - return pfs.del(tmpPath, this.tmpPath).then(() => { - - // 4.) resolve again - return this.resolve(resource); - }); - }); - }); - }).then(null, error => { - if (this.options.verboseLogging) { - this.options.errorLogger(`Unable to write to file '${resource.toString(true)}' as elevated user (${error})`); - } - - if (!FileOperationError.isFileOperationError(error)) { - error = new FileOperationError( - nls.localize('filePermission', "Permission denied writing to file ({0})", resource.toString(true)), - FileOperationResult.FILE_PERMISSION_DENIED, - options - ); - } - - return TPromise.wrapError(error); - }); - } - - public createFile(resource: uri, content: string = '', options: ICreateFileOptions = Object.create(null)): TPromise { - const absolutePath = this.toAbsolutePath(resource); - - let checkFilePromise: TPromise; - if (options.overwrite) { - checkFilePromise = TPromise.as(false); - } else { - checkFilePromise = pfs.exists(absolutePath); - } - - // Check file exists - return checkFilePromise.then(exists => { - if (exists && !options.overwrite) { - return TPromise.wrapError(new FileOperationError( - nls.localize('fileExists', "File to create already exists ({0})", resource.toString(true)), - FileOperationResult.FILE_MODIFIED_SINCE, - options - )); - } - - // Create file - return this.updateContent(resource, content).then(result => { - - // Events - this._onAfterOperation.fire(new FileOperationEvent(resource, FileOperation.CREATE, result)); - - return result; - }); - }); - } - - public createFolder(resource: uri): TPromise { - - // 1.) Create folder - const absolutePath = this.toAbsolutePath(resource); - return pfs.mkdirp(absolutePath).then(() => { - - // 2.) Resolve - return this.resolve(resource).then(result => { - - // Events - this._onAfterOperation.fire(new FileOperationEvent(resource, FileOperation.CREATE, result)); - - return result; - }); - }); - } - - public touchFile(resource: uri): TPromise { - const absolutePath = this.toAbsolutePath(resource); - - // 1.) check file - return this.checkFile(absolutePath).then(exists => { - let createPromise: TPromise; - if (exists) { - createPromise = TPromise.as(null); - } else { - createPromise = this.createFile(resource); - } - - // 2.) create file as needed - return createPromise.then(() => { - - // 3.) update atime and mtime - return pfs.touch(absolutePath).then(() => { - - // 4.) resolve - return this.resolve(resource); - }); - }); - }); - } - - public rename(resource: uri, newName: string): TPromise { - const newPath = paths.join(paths.dirname(resource.fsPath), newName); - - return this.moveFile(resource, uri.file(newPath)); - } - - public moveFile(source: uri, target: uri, overwrite?: boolean): TPromise { - return this.moveOrCopyFile(source, target, false, overwrite); - } - - public copyFile(source: uri, target: uri, overwrite?: boolean): TPromise { - return this.moveOrCopyFile(source, target, true, overwrite); - } - - private moveOrCopyFile(source: uri, target: uri, keepCopy: boolean, overwrite: boolean): TPromise { - const sourcePath = this.toAbsolutePath(source); - const targetPath = this.toAbsolutePath(target); - - // 1.) move / copy - return this.doMoveOrCopyFile(sourcePath, targetPath, keepCopy, overwrite).then(() => { - - // 2.) resolve - return this.resolve(target).then(result => { - - // Events - this._onAfterOperation.fire(new FileOperationEvent(source, keepCopy ? FileOperation.COPY : FileOperation.MOVE, result)); - - return result; - }); - }); - } - - private doMoveOrCopyFile(sourcePath: string, targetPath: string, keepCopy: boolean, overwrite: boolean): TPromise { - - // 1.) validate operation - if (isParent(targetPath, sourcePath, !isLinux)) { - return TPromise.wrapError(new Error('Unable to move/copy when source path is parent of target path')); - } - - // 2.) check if target exists - return pfs.exists(targetPath).then(exists => { - const isCaseRename = sourcePath.toLowerCase() === targetPath.toLowerCase(); - const isSameFile = sourcePath === targetPath; - - // Return early with conflict if target exists and we are not told to overwrite - if (exists && !isCaseRename && !overwrite) { - return TPromise.wrapError(new FileOperationError(nls.localize('fileMoveConflict', "Unable to move/copy. File already exists at destination."), FileOperationResult.FILE_MOVE_CONFLICT)); - } - - // 3.) make sure target is deleted before we move/copy unless this is a case rename of the same file - let deleteTargetPromise = TPromise.wrap(void 0); - if (exists && !isCaseRename) { - if (isEqualOrParent(sourcePath, targetPath, !isLinux /* ignorecase */)) { - return TPromise.wrapError(new Error(nls.localize('unableToMoveCopyError', "Unable to move/copy. File would replace folder it is contained in."))); // catch this corner case! - } - - deleteTargetPromise = this.del(uri.file(targetPath)); - } - - return deleteTargetPromise.then(() => { - - // 4.) make sure parents exists - return pfs.mkdirp(paths.dirname(targetPath)).then(() => { - - // 4.) copy/move - if (isSameFile) { - return TPromise.wrap(null); - } else if (keepCopy) { - return nfcall(extfs.copy, sourcePath, targetPath); - } else { - return nfcall(extfs.mv, sourcePath, targetPath); - } - }).then(() => exists); - }); - }); - } - - public importFile(source: uri, targetFolder: uri): TPromise { - const sourcePath = this.toAbsolutePath(source); - const targetResource = uri.file(paths.join(targetFolder.fsPath, paths.basename(source.fsPath))); - const targetPath = this.toAbsolutePath(targetResource); - - // 1.) resolve - return pfs.stat(sourcePath).then(stat => { - if (stat.isDirectory()) { - return TPromise.wrapError(new Error(nls.localize('foldersCopyError', "Folders cannot be copied into the workspace. Please select individual files to copy them."))); // for now we do not allow to import a folder into a workspace - } - - // 2.) copy - return this.doMoveOrCopyFile(sourcePath, targetPath, true, true).then(exists => { - - // 3.) resolve - return this.resolve(targetResource).then(stat => { - - // Events - this._onAfterOperation.fire(new FileOperationEvent(source, FileOperation.IMPORT, stat)); - - return { isNew: !exists, stat: stat }; - }); - }); - }); - } - - public del(resource: uri): TPromise { - const absolutePath = this.toAbsolutePath(resource); - - return pfs.del(absolutePath, this.tmpPath).then(() => { - - // Events - this._onAfterOperation.fire(new FileOperationEvent(resource, FileOperation.DELETE)); - }); - } - - // Helpers - - private toAbsolutePath(arg1: uri | IFileStat): string { - let resource: uri; - if (arg1 instanceof uri) { - resource = arg1; - } else { - resource = (arg1).resource; - } - - assert.ok(resource && resource.scheme === Schemas.file, `Invalid resource: ${resource}`); - - return paths.normalize(resource.fsPath); - } - - private resolve(resource: uri, options: IResolveFileOptions = Object.create(null)): TPromise { - return this.toStatResolver(resource) - .then(model => model.resolve(options)); - } - - private toStatResolver(resource: uri): TPromise { - const absolutePath = this.toAbsolutePath(resource); - - return pfs.statLink(absolutePath).then(({ isSymbolicLink, stat }) => { - return new StatResolver(resource, isSymbolicLink, stat.isDirectory(), stat.mtime.getTime(), stat.size, this.options.verboseLogging ? this.options.errorLogger : void 0); - }); - } - - private getPeferredEncoding(resource: uri, options: IResolveContentOptions, detected: IMimeAndEncoding): string { - let preferredEncoding: string; - if (options && options.encoding) { - if (detected.encoding === encoding.UTF8 && options.encoding === encoding.UTF8) { - preferredEncoding = encoding.UTF8_with_bom; // indicate the file has BOM if we are to resolve with UTF 8 - } else { - preferredEncoding = options.encoding; // give passed in encoding highest priority - } - } else if (detected.encoding) { - if (detected.encoding === encoding.UTF8) { - preferredEncoding = encoding.UTF8_with_bom; // if we detected UTF-8, it can only be because of a BOM - } else { - preferredEncoding = detected.encoding; - } - } else if (this.configuredEncoding(resource) === encoding.UTF8_with_bom) { - preferredEncoding = encoding.UTF8; // if we did not detect UTF 8 BOM before, this can only be UTF 8 then - } - return preferredEncoding; - } - - public getEncoding(resource: uri, preferredEncoding?: string): string { - let fileEncoding: string; - - const override = this.getEncodingOverride(resource); - if (override) { - fileEncoding = override; - } else if (preferredEncoding) { - fileEncoding = preferredEncoding; - } else { - fileEncoding = this.configuredEncoding(resource); - } - - if (!fileEncoding || !encoding.encodingExists(fileEncoding)) { - fileEncoding = encoding.UTF8; // the default is UTF 8 - } - - return fileEncoding; - } - - private configuredAutoGuessEncoding(resource: uri): boolean { - return this.textResourceConfigurationService.getValue(resource, 'files.autoGuessEncoding'); - } - - private configuredEncoding(resource: uri): string { - return this.textResourceConfigurationService.getValue(resource, 'files.encoding'); - } - - private getEncodingOverride(resource: uri): string { - if (resource && this.options.encodingOverride && this.options.encodingOverride.length) { - for (let i = 0; i < this.options.encodingOverride.length; i++) { - const override = this.options.encodingOverride[i]; - - // check if the resource is a child of the resource with override and use - // the provided encoding in that case - if (isParent(resource.fsPath, override.resource.fsPath, !isLinux /* ignorecase */)) { - return override.encoding; - } - } - } - - return null; - } - - private checkFile(absolutePath: string, options: IUpdateContentOptions = Object.create(null), ignoreReadonly?: boolean): TPromise { - return pfs.exists(absolutePath).then(exists => { - if (exists) { - return pfs.stat(absolutePath).then(stat => { - if (stat.isDirectory()) { - return TPromise.wrapError(new Error('Expected file is actually a directory')); - } - - // Dirty write prevention - if (typeof options.mtime === 'number' && typeof options.etag === 'string' && options.mtime < stat.mtime.getTime()) { - - // Find out if content length has changed - if (options.etag !== etag(stat.size, options.mtime)) { - return TPromise.wrapError(new FileOperationError(nls.localize('fileModifiedError', "File Modified Since"), FileOperationResult.FILE_MODIFIED_SINCE, options)); - } - } - - // Throw if file is readonly and we are not instructed to overwrite - if (!ignoreReadonly && !(stat.mode & 128) /* readonly */) { - if (!options.overwriteReadonly) { - return this.readOnlyError(options); - } - - // Try to change mode to writeable - let mode = stat.mode; - mode = mode | 128; - return pfs.chmod(absolutePath, mode).then(() => { - - // Make sure to check the mode again, it could have failed - return pfs.stat(absolutePath).then(stat => { - if (!(stat.mode & 128) /* readonly */) { - return this.readOnlyError(options); - } - - return exists; - }); - }); - } - - return TPromise.as(exists); - }); - } - - return TPromise.as(exists); - }); - } - - private readOnlyError(options: IUpdateContentOptions): TPromise { - return TPromise.wrapError(new FileOperationError( - nls.localize('fileReadOnlyError', "File is Read Only"), - FileOperationResult.FILE_READ_ONLY, - options - )); - } - - public watchFileChanges(resource: uri): void { - assert.ok(resource && resource.scheme === Schemas.file, `Invalid resource for watching: ${resource}`); - - // Create or get watcher for provided path - let watcher = this.activeFileChangesWatchers.get(resource); - if (!watcher) { - const fsPath = resource.fsPath; - const fsName = paths.basename(resource.fsPath); - - watcher = extfs.watch(fsPath, (eventType: string, filename: string) => { - const renamedOrDeleted = ((filename && filename !== fsName) || eventType === 'rename'); - - // The file was either deleted or renamed. Many tools apply changes to files in an - // atomic way ("Atomic Save") by first renaming the file to a temporary name and then - // renaming it back to the original name. Our watcher will detect this as a rename - // and then stops to work on Mac and Linux because the watcher is applied to the - // inode and not the name. The fix is to detect this case and trying to watch the file - // again after a certain delay. - // In addition, we send out a delete event if after a timeout we detect that the file - // does indeed not exist anymore. - if (renamedOrDeleted) { - - // Very important to dispose the watcher which now points to a stale inode - this.unwatchFileChanges(resource); - - // Wait a bit and try to install watcher again, assuming that the file was renamed quickly ("Atomic Save") - setTimeout(() => { - this.existsFile(resource).done(exists => { - - // File still exists, so reapply the watcher - if (exists) { - this.watchFileChanges(resource); - } - - // File seems to be really gone, so emit a deleted event - else { - this.onRawFileChange({ - type: FileChangeType.DELETED, - path: fsPath - }); - } - }); - }, FileService.FS_REWATCH_DELAY); - } - - // Handle raw file change - this.onRawFileChange({ - type: FileChangeType.UPDATED, - path: fsPath - }); - }, (error: string) => this.options.errorLogger(error)); - - if (watcher) { - this.activeFileChangesWatchers.set(resource, watcher); - } - } - } - - private onRawFileChange(event: IRawFileChange): void { - - // add to bucket of undelivered events - this.undeliveredRawFileChangesEvents.push(event); - - if (this.options.verboseLogging) { - console.log('%c[node.js Watcher]%c', 'color: green', 'color: black', event.type === FileChangeType.ADDED ? '[ADDED]' : event.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]', event.path); - } - - // handle emit through delayer to accommodate for bulk changes - this.fileChangesWatchDelayer.trigger(() => { - const buffer = this.undeliveredRawFileChangesEvents; - this.undeliveredRawFileChangesEvents = []; - - // Normalize - const normalizedEvents = normalize(buffer); - - // Logging - if (this.options.verboseLogging) { - normalizedEvents.forEach(r => { - console.log('%c[node.js Watcher]%c >> normalized', 'color: green', 'color: black', r.type === FileChangeType.ADDED ? '[ADDED]' : r.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]', r.path); - }); - } - - // Emit - this._onFileChanges.fire(toFileChangesEvent(normalizedEvents)); - - return TPromise.as(null); - }); - } - - public unwatchFileChanges(resource: uri): void { - const watcher = this.activeFileChangesWatchers.get(resource); - if (watcher) { - watcher.close(); - this.activeFileChangesWatchers.delete(resource); - } - } - - public dispose(): void { - this.toDispose = dispose(this.toDispose); - - if (this.activeWorkspaceFileChangeWatcher) { - this.activeWorkspaceFileChangeWatcher.dispose(); - this.activeWorkspaceFileChangeWatcher = null; - } - - this.activeFileChangesWatchers.forEach(watcher => watcher.close()); - this.activeFileChangesWatchers.clear(); - } -} - -export class StatResolver { - private name: string; - private etag: string; - - constructor( - private resource: uri, - private isSymbolicLink: boolean, - private isDirectory: boolean, - private mtime: number, - private size: number, - private errorLogger?: (error: Error | string) => void - ) { - assert.ok(resource && resource.scheme === Schemas.file, `Invalid resource: ${resource}`); - - this.name = getBaseLabel(resource); - this.etag = etag(size, mtime); - } - - public resolve(options: IResolveFileOptions): TPromise { - - // General Data - const fileStat: IFileStat = { - resource: this.resource, - isDirectory: this.isDirectory, - isSymbolicLink: this.isSymbolicLink, - name: this.name, - etag: this.etag, - size: this.size, - mtime: this.mtime - }; - - // File Specific Data - if (!this.isDirectory) { - return TPromise.as(fileStat); - } - - // Directory Specific Data - else { - - // Convert the paths from options.resolveTo to absolute paths - let absoluteTargetPaths: string[] = null; - if (options && options.resolveTo) { - absoluteTargetPaths = []; - options.resolveTo.forEach(resource => { - absoluteTargetPaths.push(resource.fsPath); - }); - } - - return new TPromise((c, e) => { - - // Load children - this.resolveChildren(this.resource.fsPath, absoluteTargetPaths, options && options.resolveSingleChildDescendants, children => { - children = arrays.coalesce(children); // we don't want those null children (could be permission denied when reading a child) - fileStat.children = children || []; - - c(fileStat); - }); - }); - } - } - - private resolveChildren(absolutePath: string, absoluteTargetPaths: string[], resolveSingleChildDescendants: boolean, callback: (children: IFileStat[]) => void): void { - extfs.readdir(absolutePath, (error: Error, files: string[]) => { - if (error) { - if (this.errorLogger) { - this.errorLogger(error); - } - - return callback(null); // return - we might not have permissions to read the folder - } - - // for each file in the folder - flow.parallel(files, (file: string, clb: (error: Error, children: IFileStat) => void) => { - const fileResource = uri.file(paths.resolve(absolutePath, file)); - let fileStat: fs.Stats; - let isSymbolicLink = false; - const $this = this; - - flow.sequence( - function onError(error: Error): void { - if ($this.errorLogger) { - $this.errorLogger(error); - } - - clb(null, null); // return - we might not have permissions to read the folder or stat the file - }, - - function stat(this: any): void { - extfs.statLink(fileResource.fsPath, this); - }, - - function countChildren(this: any, statAndLink: extfs.IStatAndLink): void { - fileStat = statAndLink.stat; - isSymbolicLink = statAndLink.isSymbolicLink; - - if (fileStat.isDirectory()) { - extfs.readdir(fileResource.fsPath, (error, result) => { - this(null, result ? result.length : 0); - }); - } else { - this(null, 0); - } - }, - - function resolve(childCount: number): void { - const childStat: IFileStat = { - resource: fileResource, - isDirectory: fileStat.isDirectory(), - isSymbolicLink, - name: file, - mtime: fileStat.mtime.getTime(), - etag: etag(fileStat), - size: fileStat.size - }; - - // Return early for files - if (!fileStat.isDirectory()) { - return clb(null, childStat); - } - - // Handle Folder - let resolveFolderChildren = false; - if (files.length === 1 && resolveSingleChildDescendants) { - resolveFolderChildren = true; - } else if (childCount > 0 && absoluteTargetPaths && absoluteTargetPaths.some(targetPath => isEqualOrParent(targetPath, fileResource.fsPath, !isLinux /* ignorecase */))) { - resolveFolderChildren = true; - } - - // Continue resolving children based on condition - if (resolveFolderChildren) { - $this.resolveChildren(fileResource.fsPath, absoluteTargetPaths, resolveSingleChildDescendants, children => { - children = arrays.coalesce(children); // we don't want those null children - childStat.children = children || []; - - clb(null, childStat); - }); - } - - // Otherwise return result - else { - clb(null, childStat); - } - }); - }, (errors, result) => { - callback(result); - }); - }); - } -} diff --git a/src/vs/workbench/services/files/node/watcher/nsfw/nsfwWatcherService.ts b/src/vs/workbench/services/files/node/watcher/nsfw/nsfwWatcherService.ts index 6b85c158939..5e73b6cbaeb 100644 --- a/src/vs/workbench/services/files/node/watcher/nsfw/nsfwWatcherService.ts +++ b/src/vs/workbench/services/files/node/watcher/nsfw/nsfwWatcherService.ts @@ -61,7 +61,7 @@ export class NsfwWatcherService implements IWatcherService { ignored: request.ignored }; - process.on('uncaughtException', e => { + process.on('uncaughtException', (e: Error | string) => { // Specially handle ENOSPC errors that can happen when // the watcher consumes so many file descriptors that diff --git a/src/vs/workbench/services/files/node/watcher/unix/chokidarWatcherService.ts b/src/vs/workbench/services/files/node/watcher/unix/chokidarWatcherService.ts index 191689d3615..175ef1a62ec 100644 --- a/src/vs/workbench/services/files/node/watcher/unix/chokidarWatcherService.ts +++ b/src/vs/workbench/services/files/node/watcher/unix/chokidarWatcherService.ts @@ -19,6 +19,7 @@ import { realcaseSync } from 'vs/base/node/extfs'; import { isMacintosh } from 'vs/base/common/platform'; import * as watcher from 'vs/workbench/services/files/node/watcher/common'; import { IWatcherRequest, IWatcherService } from 'vs/workbench/services/files/node/watcher/unix/watcher'; +import { IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; export class ChokidarWatcherService implements IWatcherService { @@ -28,6 +29,7 @@ export class ChokidarWatcherService implements IWatcherService { private spamCheckStartTime: number; private spamWarningLogged: boolean; private enospcErrorLogged: boolean; + private toDispose: IDisposable[] = []; public watch(request: IWatcherRequest): TPromise { const watcherOpts: chokidar.IOptions = { @@ -62,6 +64,11 @@ export class ChokidarWatcherService implements IWatcherService { let undeliveredFileEvents: watcher.IRawFileChange[] = []; const fileEventDelayer = new ThrottledDelayer(ChokidarWatcherService.FS_EVENT_DELAY); + this.toDispose.push(toDisposable(() => { + chokidarWatcher.close(); + fileEventDelayer.cancel(); + })); + return new TPromise((c, e, p) => { chokidarWatcher.on('all', (type: string, path: string) => { if (isMacintosh) { @@ -156,8 +163,12 @@ export class ChokidarWatcherService implements IWatcherService { } }); }, () => { - chokidarWatcher.close(); - fileEventDelayer.cancel(); + this.toDispose = dispose(this.toDispose); }); } + + public stop(): TPromise { + this.toDispose = dispose(this.toDispose); + return TPromise.as(void 0); + } } diff --git a/src/vs/workbench/services/files/test/node/fileService.test.ts b/src/vs/workbench/services/files/test/electron-browser/fileService.test.ts similarity index 86% rename from src/vs/workbench/services/files/test/node/fileService.test.ts rename to src/vs/workbench/services/files/test/electron-browser/fileService.test.ts index e526018a866..822b9dc356d 100644 --- a/src/vs/workbench/services/files/test/node/fileService.test.ts +++ b/src/vs/workbench/services/files/test/electron-browser/fileService.test.ts @@ -11,18 +11,19 @@ import * as os from 'os'; import * as assert from 'assert'; import { TPromise } from 'vs/base/common/winjs.base'; -import { FileService, IEncodingOverride } from 'vs/workbench/services/files/node/fileService'; +import { FileService } from 'vs/workbench/services/files/electron-browser/fileService'; import { FileOperation, FileOperationEvent, FileChangesEvent, FileOperationResult, FileOperationError } from 'vs/platform/files/common/files'; import uri from 'vs/base/common/uri'; import * as uuid from 'vs/base/common/uuid'; import * as pfs from 'vs/base/node/pfs'; import * as encodingLib from 'vs/base/node/encoding'; -import * as utils from 'vs/workbench/services/files/test/node/utils'; -import { TestEnvironmentService, TestContextService, TestTextResourceConfigurationService, getRandomTestPath, TestLifecycleService } from 'vs/workbench/test/workbenchTestServices'; +import * as utils from 'vs/workbench/services/files/test/electron-browser/utils'; +import { TestEnvironmentService, TestContextService, TestTextResourceConfigurationService, getRandomTestPath, TestLifecycleService, TestStorageService } from 'vs/workbench/test/workbenchTestServices'; +import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; import { Workspace, toWorkspaceFolders } from 'vs/platform/workspace/common/workspace'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { TextModel } from 'vs/editor/common/model/textModel'; -import { timeout } from 'vs/base/common/async'; +import { IEncodingOverride } from 'vs/workbench/services/files/electron-browser/encoding'; suite('FileService', () => { let service: FileService; @@ -35,7 +36,7 @@ suite('FileService', () => { const sourceDir = require.toUrl('./fixtures/service'); return pfs.copy(sourceDir, testDir).then(() => { - service = new FileService(new TestContextService(new Workspace(testDir, testDir, toWorkspaceFolders([{ path: testDir }]))), TestEnvironmentService, new TestTextResourceConfigurationService(), new TestConfigurationService(), new TestLifecycleService(), { disableWatcher: true }); + service = new FileService(new TestContextService(new Workspace(testDir, testDir, toWorkspaceFolders([{ path: testDir }]))), TestEnvironmentService, new TestTextResourceConfigurationService(), new TestConfigurationService(), new TestLifecycleService(), new TestStorageService(), new TestNotificationService(), { disableWatcher: true }); }); }); @@ -148,44 +149,6 @@ suite('FileService', () => { }); }); - test('touchFile', function () { - return service.touchFile(uri.file(path.join(testDir, 'test.txt'))).then(s => { - assert.equal(s.name, 'test.txt'); - assert.equal(fs.existsSync(s.resource.fsPath), true); - assert.equal(fs.readFileSync(s.resource.fsPath).length, 0); - - const stat = fs.statSync(s.resource.fsPath); - - return timeout(10).then(() => { - return service.touchFile(s.resource).then(s => { - const statNow = fs.statSync(s.resource.fsPath); - assert.ok(statNow.mtime.getTime() >= stat.mtime.getTime()); // one some OS the resolution seems to be 1s, so we use >= here - assert.equal(statNow.size, stat.size); - }); - }); - }); - }); - - test('touchFile - multi folder', function () { - const multiFolderPaths = ['a', 'couple', 'of', 'folders']; - - return service.touchFile(uri.file(path.join(testDir, ...multiFolderPaths, 'test.txt'))).then(s => { - assert.equal(s.name, 'test.txt'); - assert.equal(fs.existsSync(s.resource.fsPath), true); - assert.equal(fs.readFileSync(s.resource.fsPath).length, 0); - - const stat = fs.statSync(s.resource.fsPath); - - return timeout(10).then(() => { - return service.touchFile(s.resource).then(s => { - const statNow = fs.statSync(s.resource.fsPath); - assert.ok(statNow.mtime.getTime() >= stat.mtime.getTime()); // one some OS the resolution seems to be 1s, so we use >= here - assert.equal(statNow.size, stat.size); - }); - }); - }); - }); - test('renameFile', function () { let event: FileOperationEvent; const toDispose = service.onAfterOperation(e => { @@ -465,36 +428,18 @@ suite('FileService', () => { }); }); - test('importFile', function () { - let event: FileOperationEvent; - const toDispose = service.onAfterOperation(e => { - event = e; - }); - - return service.resolveFile(uri.file(path.join(testDir, 'deep'))).then(target => { - const resource = uri.file(require.toUrl('./fixtures/service/index.html')); - return service.importFile(resource, target.resource).then(res => { - assert.equal(res.isNew, true); - assert.equal(fs.existsSync(res.stat.resource.fsPath), true); - - assert.ok(event); - assert.equal(event.resource.fsPath, resource.fsPath); - assert.equal(event.operation, FileOperation.IMPORT); - assert.equal(event.target.resource.fsPath, res.stat.resource.fsPath); - toDispose.dispose(); - }); - }); - }); - - test('importFile - MIX CASE', function () { + test('copyFile - MIX CASE', function () { return service.resolveFile(uri.file(path.join(testDir, 'index.html'))).then(source => { return service.rename(source.resource, 'CONWAY.js').then(renamed => { // index.html => CONWAY.js assert.equal(fs.existsSync(renamed.resource.fsPath), true); assert.ok(fs.readdirSync(testDir).some(f => f === 'CONWAY.js')); return service.resolveFile(uri.file(path.join(testDir, 'deep', 'conway.js'))).then(source => { - return service.importFile(source.resource, uri.file(testDir)).then(res => { // CONWAY.js => conway.js - assert.equal(fs.existsSync(res.stat.resource.fsPath), true); + const targetParent = uri.file(testDir); + const target = targetParent.with({ path: path.posix.join(targetParent.path, path.posix.basename(source.resource.path)) }); + + return service.copyFile(source.resource, target, true).then(res => { // CONWAY.js => conway.js + assert.equal(fs.existsSync(res.resource.fsPath), true); assert.ok(fs.readdirSync(testDir).some(f => f === 'conway.js')); }); }); @@ -502,48 +447,12 @@ suite('FileService', () => { }); }); - test('importFile - overwrite folder with file', function () { - let createEvent: FileOperationEvent; - let importEvent: FileOperationEvent; - let deleteEvent: FileOperationEvent; - const toDispose = service.onAfterOperation(e => { - if (e.operation === FileOperation.CREATE) { - createEvent = e; - } else if (e.operation === FileOperation.DELETE) { - deleteEvent = e; - } else if (e.operation === FileOperation.IMPORT) { - importEvent = e; - } - }); - - return service.resolveFile(uri.file(testDir)).then(parent => { - const folderResource = uri.file(path.join(parent.resource.fsPath, 'conway.js')); - return service.createFolder(folderResource).then(f => { - const resource = uri.file(path.join(testDir, 'deep', 'conway.js')); - return service.importFile(resource, uri.file(testDir)).then(res => { - assert.equal(fs.existsSync(res.stat.resource.fsPath), true); - assert.ok(fs.readdirSync(testDir).some(f => f === 'conway.js')); - assert.ok(fs.statSync(res.stat.resource.fsPath).isFile); - - assert.ok(createEvent); - assert.ok(deleteEvent); - assert.ok(importEvent); - - assert.equal(importEvent.resource.fsPath, resource.fsPath); - assert.equal(importEvent.target.resource.fsPath, res.stat.resource.fsPath); - - assert.equal(deleteEvent.resource.fsPath, folderResource.fsPath); - - toDispose.dispose(); - }); - }); - }); - }); - - test('importFile - same file', function () { + test('copyFile - same file', function () { return service.resolveFile(uri.file(path.join(testDir, 'index.html'))).then(source => { - return service.importFile(source.resource, uri.file(path.dirname(source.resource.fsPath))).then(imported => { - assert.equal(imported.stat.size, source.size); + const targetParent = uri.file(path.dirname(source.resource.fsPath)); + const target = targetParent.with({ path: path.posix.join(targetParent.path, path.posix.basename(source.resource.path)) }); + return service.copyFile(source.resource, target, true).then(copied => { + assert.equal(copied.size, source.size); }); }); }); @@ -912,7 +821,7 @@ suite('FileService', () => { }, 100); }); - test('options - encoding', function () { + test('options - encoding override (parent)', function () { // setup const _id = uuid.generateUuid(); @@ -922,7 +831,7 @@ suite('FileService', () => { return pfs.copy(_sourceDir, _testDir).then(() => { const encodingOverride: IEncodingOverride[] = []; encodingOverride.push({ - resource: uri.file(path.join(testDir, 'deep')), + parent: uri.file(path.join(testDir, 'deep')), encoding: 'utf16le' }); @@ -931,10 +840,63 @@ suite('FileService', () => { const textResourceConfigurationService = new TestTextResourceConfigurationService(configurationService); - const _service = new FileService(new TestContextService(new Workspace(_testDir, _testDir, toWorkspaceFolders([{ path: _testDir }]))), TestEnvironmentService, textResourceConfigurationService, configurationService, new TestLifecycleService(), { - encodingOverride, - disableWatcher: true + const _service = new FileService( + new TestContextService(new Workspace(_testDir, _testDir, toWorkspaceFolders([{ path: _testDir }]))), + TestEnvironmentService, + textResourceConfigurationService, + configurationService, + new TestLifecycleService(), + new TestStorageService(), + new TestNotificationService(), + { + encodingOverride, + disableWatcher: true + }); + + return _service.resolveContent(uri.file(path.join(testDir, 'index.html'))).then(c => { + assert.equal(c.encoding, 'windows1252'); + + return _service.resolveContent(uri.file(path.join(testDir, 'deep', 'conway.js'))).then(c => { + assert.equal(c.encoding, 'utf16le'); + + // teardown + _service.dispose(); + }); }); + }); + }); + + test('options - encoding override (extension)', function () { + + // setup + const _id = uuid.generateUuid(); + const _testDir = path.join(parentDir, _id); + const _sourceDir = require.toUrl('./fixtures/service'); + + return pfs.copy(_sourceDir, _testDir).then(() => { + const encodingOverride: IEncodingOverride[] = []; + encodingOverride.push({ + extension: 'js', + encoding: 'utf16le' + }); + + const configurationService = new TestConfigurationService(); + configurationService.setUserConfiguration('files', { encoding: 'windows1252' }); + + const textResourceConfigurationService = new TestTextResourceConfigurationService(configurationService); + + const _service = new FileService( + new TestContextService(new Workspace(_testDir, _testDir, toWorkspaceFolders([{ path: _testDir }]))), + TestEnvironmentService, + textResourceConfigurationService, + configurationService, + new TestLifecycleService(), + new TestStorageService(), + new TestNotificationService(), + { + encodingOverride, + disableWatcher: true + }); return _service.resolveContent(uri.file(path.join(testDir, 'index.html'))).then(c => { assert.equal(c.encoding, 'windows1252'); @@ -957,9 +919,17 @@ suite('FileService', () => { const _sourceDir = require.toUrl('./fixtures/service'); const resource = uri.file(path.join(testDir, 'index.html')); - const _service = new FileService(new TestContextService(new Workspace(_testDir, _testDir, toWorkspaceFolders([{ path: _testDir }]))), TestEnvironmentService, new TestTextResourceConfigurationService(), new TestConfigurationService(), new TestLifecycleService(), { - disableWatcher: true - }); + const _service = new FileService( + new TestContextService(new Workspace(_testDir, _testDir, toWorkspaceFolders([{ path: _testDir }]))), + TestEnvironmentService, + new TestTextResourceConfigurationService(), + new TestConfigurationService(), + new TestLifecycleService(), + new TestStorageService(), + new TestNotificationService(), + { + disableWatcher: true + }); return pfs.copy(_sourceDir, _testDir).then(() => { return pfs.readFile(resource.fsPath).then(data => { diff --git a/src/vs/workbench/services/files/test/node/fixtures/resolver/examples/company.js b/src/vs/workbench/services/files/test/electron-browser/fixtures/resolver/examples/company.js similarity index 100% rename from src/vs/workbench/services/files/test/node/fixtures/resolver/examples/company.js rename to src/vs/workbench/services/files/test/electron-browser/fixtures/resolver/examples/company.js diff --git a/src/vs/workbench/services/files/test/node/fixtures/resolver/examples/conway.js b/src/vs/workbench/services/files/test/electron-browser/fixtures/resolver/examples/conway.js similarity index 100% rename from src/vs/workbench/services/files/test/node/fixtures/resolver/examples/conway.js rename to src/vs/workbench/services/files/test/electron-browser/fixtures/resolver/examples/conway.js diff --git a/src/vs/workbench/services/files/test/node/fixtures/resolver/examples/employee.js b/src/vs/workbench/services/files/test/electron-browser/fixtures/resolver/examples/employee.js similarity index 100% rename from src/vs/workbench/services/files/test/node/fixtures/resolver/examples/employee.js rename to src/vs/workbench/services/files/test/electron-browser/fixtures/resolver/examples/employee.js diff --git a/src/vs/workbench/services/files/test/node/fixtures/resolver/examples/small.js b/src/vs/workbench/services/files/test/electron-browser/fixtures/resolver/examples/small.js similarity index 100% rename from src/vs/workbench/services/files/test/node/fixtures/resolver/examples/small.js rename to src/vs/workbench/services/files/test/electron-browser/fixtures/resolver/examples/small.js diff --git a/src/vs/workbench/services/files/test/node/fixtures/resolver/index.html b/src/vs/workbench/services/files/test/electron-browser/fixtures/resolver/index.html similarity index 100% rename from src/vs/workbench/services/files/test/node/fixtures/resolver/index.html rename to src/vs/workbench/services/files/test/electron-browser/fixtures/resolver/index.html diff --git a/src/vs/workbench/services/files/test/node/fixtures/resolver/other/deep/company.js b/src/vs/workbench/services/files/test/electron-browser/fixtures/resolver/other/deep/company.js similarity index 100% rename from src/vs/workbench/services/files/test/node/fixtures/resolver/other/deep/company.js rename to src/vs/workbench/services/files/test/electron-browser/fixtures/resolver/other/deep/company.js diff --git a/src/vs/workbench/services/files/test/node/fixtures/resolver/other/deep/conway.js b/src/vs/workbench/services/files/test/electron-browser/fixtures/resolver/other/deep/conway.js similarity index 100% rename from src/vs/workbench/services/files/test/node/fixtures/resolver/other/deep/conway.js rename to src/vs/workbench/services/files/test/electron-browser/fixtures/resolver/other/deep/conway.js diff --git a/src/vs/workbench/services/files/test/node/fixtures/resolver/other/deep/employee.js b/src/vs/workbench/services/files/test/electron-browser/fixtures/resolver/other/deep/employee.js similarity index 100% rename from src/vs/workbench/services/files/test/node/fixtures/resolver/other/deep/employee.js rename to src/vs/workbench/services/files/test/electron-browser/fixtures/resolver/other/deep/employee.js diff --git a/src/vs/workbench/services/files/test/node/fixtures/resolver/other/deep/small.js b/src/vs/workbench/services/files/test/electron-browser/fixtures/resolver/other/deep/small.js similarity index 100% rename from src/vs/workbench/services/files/test/node/fixtures/resolver/other/deep/small.js rename to src/vs/workbench/services/files/test/electron-browser/fixtures/resolver/other/deep/small.js diff --git a/src/vs/workbench/services/files/test/node/fixtures/resolver/site.css b/src/vs/workbench/services/files/test/electron-browser/fixtures/resolver/site.css similarity index 100% rename from src/vs/workbench/services/files/test/node/fixtures/resolver/site.css rename to src/vs/workbench/services/files/test/electron-browser/fixtures/resolver/site.css diff --git a/src/vs/workbench/services/files/test/node/fixtures/service/binary.txt b/src/vs/workbench/services/files/test/electron-browser/fixtures/service/binary.txt similarity index 100% rename from src/vs/workbench/services/files/test/node/fixtures/service/binary.txt rename to src/vs/workbench/services/files/test/electron-browser/fixtures/service/binary.txt diff --git a/src/vs/workbench/services/files/test/node/fixtures/service/deep/company.js b/src/vs/workbench/services/files/test/electron-browser/fixtures/service/deep/company.js similarity index 100% rename from src/vs/workbench/services/files/test/node/fixtures/service/deep/company.js rename to src/vs/workbench/services/files/test/electron-browser/fixtures/service/deep/company.js diff --git a/src/vs/workbench/services/files/test/node/fixtures/service/deep/conway.js b/src/vs/workbench/services/files/test/electron-browser/fixtures/service/deep/conway.js similarity index 100% rename from src/vs/workbench/services/files/test/node/fixtures/service/deep/conway.js rename to src/vs/workbench/services/files/test/electron-browser/fixtures/service/deep/conway.js diff --git a/src/vs/workbench/services/files/test/node/fixtures/service/deep/employee.js b/src/vs/workbench/services/files/test/electron-browser/fixtures/service/deep/employee.js similarity index 100% rename from src/vs/workbench/services/files/test/node/fixtures/service/deep/employee.js rename to src/vs/workbench/services/files/test/electron-browser/fixtures/service/deep/employee.js diff --git a/src/vs/workbench/services/files/test/node/fixtures/service/deep/small.js b/src/vs/workbench/services/files/test/electron-browser/fixtures/service/deep/small.js similarity index 100% rename from src/vs/workbench/services/files/test/node/fixtures/service/deep/small.js rename to src/vs/workbench/services/files/test/electron-browser/fixtures/service/deep/small.js diff --git a/src/vs/workbench/services/files/test/node/fixtures/service/index.html b/src/vs/workbench/services/files/test/electron-browser/fixtures/service/index.html similarity index 100% rename from src/vs/workbench/services/files/test/node/fixtures/service/index.html rename to src/vs/workbench/services/files/test/electron-browser/fixtures/service/index.html diff --git a/src/vs/workbench/services/files/test/node/fixtures/service/lorem.txt b/src/vs/workbench/services/files/test/electron-browser/fixtures/service/lorem.txt similarity index 100% rename from src/vs/workbench/services/files/test/node/fixtures/service/lorem.txt rename to src/vs/workbench/services/files/test/electron-browser/fixtures/service/lorem.txt diff --git a/src/vs/workbench/services/files/test/node/fixtures/service/small.txt b/src/vs/workbench/services/files/test/electron-browser/fixtures/service/small.txt similarity index 100% rename from src/vs/workbench/services/files/test/node/fixtures/service/small.txt rename to src/vs/workbench/services/files/test/electron-browser/fixtures/service/small.txt diff --git a/src/vs/workbench/services/files/test/node/fixtures/service/small_umlaut.txt b/src/vs/workbench/services/files/test/electron-browser/fixtures/service/small_umlaut.txt similarity index 100% rename from src/vs/workbench/services/files/test/node/fixtures/service/small_umlaut.txt rename to src/vs/workbench/services/files/test/electron-browser/fixtures/service/small_umlaut.txt diff --git a/src/vs/workbench/services/files/test/node/fixtures/service/some_utf16le.css b/src/vs/workbench/services/files/test/electron-browser/fixtures/service/some_utf16le.css similarity index 100% rename from src/vs/workbench/services/files/test/node/fixtures/service/some_utf16le.css rename to src/vs/workbench/services/files/test/electron-browser/fixtures/service/some_utf16le.css diff --git a/src/vs/workbench/services/files/test/node/fixtures/service/some_utf8_bom.txt b/src/vs/workbench/services/files/test/electron-browser/fixtures/service/some_utf8_bom.txt similarity index 100% rename from src/vs/workbench/services/files/test/node/fixtures/service/some_utf8_bom.txt rename to src/vs/workbench/services/files/test/electron-browser/fixtures/service/some_utf8_bom.txt diff --git a/src/vs/workbench/services/files/test/node/resolver.test.ts b/src/vs/workbench/services/files/test/electron-browser/resolver.test.ts similarity index 97% rename from src/vs/workbench/services/files/test/node/resolver.test.ts rename to src/vs/workbench/services/files/test/electron-browser/resolver.test.ts index 230d31d4e0f..3a9c19ad869 100644 --- a/src/vs/workbench/services/files/test/node/resolver.test.ts +++ b/src/vs/workbench/services/files/test/electron-browser/resolver.test.ts @@ -9,10 +9,10 @@ import * as fs from 'fs'; import * as path from 'path'; import * as assert from 'assert'; -import { StatResolver } from 'vs/workbench/services/files/node/fileService'; +import { StatResolver } from 'vs/workbench/services/files/electron-browser/fileService'; import uri from 'vs/base/common/uri'; import { isLinux } from 'vs/base/common/platform'; -import * as utils from 'vs/workbench/services/files/test/node/utils'; +import * as utils from 'vs/workbench/services/files/test/electron-browser/utils'; function create(relativePath: string): StatResolver { let basePath = require.toUrl('./fixtures/resolver'); diff --git a/src/vs/workbench/services/files/test/node/utils.ts b/src/vs/workbench/services/files/test/electron-browser/utils.ts similarity index 100% rename from src/vs/workbench/services/files/test/node/utils.ts rename to src/vs/workbench/services/files/test/electron-browser/utils.ts diff --git a/src/vs/workbench/services/files/test/node/watcher.test.ts b/src/vs/workbench/services/files/test/electron-browser/watcher.test.ts similarity index 100% rename from src/vs/workbench/services/files/test/node/watcher.test.ts rename to src/vs/workbench/services/files/test/electron-browser/watcher.test.ts diff --git a/src/vs/workbench/services/issue/common/issue.ts b/src/vs/workbench/services/issue/common/issue.ts index b998241ce1c..e97d68ea091 100644 --- a/src/vs/workbench/services/issue/common/issue.ts +++ b/src/vs/workbench/services/issue/common/issue.ts @@ -14,4 +14,5 @@ export const IWorkbenchIssueService = createDecorator('w export interface IWorkbenchIssueService { _serviceBrand: any; openReporter(dataOverrides?: Partial): TPromise; + openProcessExplorer(): TPromise; } diff --git a/src/vs/workbench/services/issue/electron-browser/workbenchIssueService.ts b/src/vs/workbench/services/issue/electron-browser/workbenchIssueService.ts index 7e78f2904a5..80d5a39cad2 100644 --- a/src/vs/workbench/services/issue/electron-browser/workbenchIssueService.ts +++ b/src/vs/workbench/services/issue/electron-browser/workbenchIssueService.ts @@ -8,7 +8,7 @@ import { IssueReporterStyles, IIssueService, IssueReporterData } from 'vs/platform/issue/common/issue'; import { TPromise } from 'vs/base/common/winjs.base'; import { ITheme, IThemeService } from 'vs/platform/theme/common/themeService'; -import { textLinkForeground, inputBackground, inputBorder, inputForeground, buttonBackground, buttonHoverBackground, buttonForeground, inputValidationErrorBorder, foreground, inputActiveOptionBorder, scrollbarSliderActiveBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground } from 'vs/platform/theme/common/colorRegistry'; +import { textLinkForeground, inputBackground, inputBorder, inputForeground, buttonBackground, buttonHoverBackground, buttonForeground, inputValidationErrorBorder, foreground, inputActiveOptionBorder, scrollbarSliderActiveBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground, editorBackground, editorForeground, listHoverBackground, listHoverForeground, listHighlightForeground } from 'vs/platform/theme/common/colorRegistry'; import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; import { IExtensionManagementService, IExtensionEnablementService, LocalExtensionType } from 'vs/platform/extensionManagement/common/extensionManagement'; import { webFrame } from 'electron'; @@ -41,6 +41,21 @@ export class WorkbenchIssueService implements IWorkbenchIssueService { return this.issueService.openReporter(issueReporterData); }); } + + openProcessExplorer(): TPromise { + const theme = this.themeService.getTheme(); + const data = { + zoomLevel: webFrame.getZoomLevel(), + styles: { + backgroundColor: theme.getColor(editorBackground) && theme.getColor(editorBackground).toString(), + color: theme.getColor(editorForeground).toString(), + hoverBackground: theme.getColor(listHoverBackground) && theme.getColor(listHoverBackground).toString(), + hoverForeground: theme.getColor(listHoverForeground) && theme.getColor(listHoverForeground).toString(), + highlightForeground: theme.getColor(listHighlightForeground) && theme.getColor(listHighlightForeground).toString() + } + }; + return this.issueService.openProcessExplorer(data); + } } export function getIssueReporterStyles(theme: ITheme): IssueReporterStyles { diff --git a/src/vs/workbench/services/keybinding/common/macLinuxKeyboardMapper.ts b/src/vs/workbench/services/keybinding/common/macLinuxKeyboardMapper.ts index 2bb8da1d890..1c56f4f0dde 100644 --- a/src/vs/workbench/services/keybinding/common/macLinuxKeyboardMapper.ts +++ b/src/vs/workbench/services/keybinding/common/macLinuxKeyboardMapper.ts @@ -74,6 +74,9 @@ export class NativeResolvedKeybinding extends ResolvedKeybinding { constructor(mapper: MacLinuxKeyboardMapper, OS: OperatingSystem, firstPart: ScanCodeBinding, chordPart: ScanCodeBinding) { super(); + if (!firstPart) { + throw new Error(`Invalid USLayoutResolvedKeybinding`); + } this._mapper = mapper; this._OS = OS; this._firstPart = firstPart; diff --git a/src/vs/workbench/services/keybinding/common/windowsKeyboardMapper.ts b/src/vs/workbench/services/keybinding/common/windowsKeyboardMapper.ts index 02c1dac816c..869932b1b18 100644 --- a/src/vs/workbench/services/keybinding/common/windowsKeyboardMapper.ts +++ b/src/vs/workbench/services/keybinding/common/windowsKeyboardMapper.ts @@ -86,6 +86,9 @@ export class WindowsNativeResolvedKeybinding extends ResolvedKeybinding { constructor(mapper: WindowsKeyboardMapper, firstPart: SimpleKeybinding, chordPart: SimpleKeybinding) { super(); + if (!firstPart) { + throw new Error(`Invalid WindowsNativeResolvedKeybinding firstPart`); + } this._mapper = mapper; this._firstPart = firstPart; this._chordPart = chordPart; diff --git a/src/vs/workbench/services/keybinding/test/node/keybindingEditing.test.ts b/src/vs/workbench/services/keybinding/test/electron-browser/keybindingEditing.test.ts similarity index 95% rename from src/vs/workbench/services/keybinding/test/node/keybindingEditing.test.ts rename to src/vs/workbench/services/keybinding/test/electron-browser/keybindingEditing.test.ts index 9e7e5d447f1..80766a0ed14 100644 --- a/src/vs/workbench/services/keybinding/test/node/keybindingEditing.test.ts +++ b/src/vs/workbench/services/keybinding/test/electron-browser/keybindingEditing.test.ts @@ -16,11 +16,12 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { KeyCode, SimpleKeybinding, ChordKeybinding } from 'vs/base/common/keyCodes'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import * as extfs from 'vs/base/node/extfs'; -import { TestTextFileService, TestEditorGroupService, TestLifecycleService, TestBackupFileService, TestContextService, TestTextResourceConfigurationService, TestHashService, TestEnvironmentService } from 'vs/workbench/test/workbenchTestServices'; +import { TestTextFileService, TestEditorGroupService, TestLifecycleService, TestBackupFileService, TestContextService, TestTextResourceConfigurationService, TestHashService, TestEnvironmentService, TestStorageService } from 'vs/workbench/test/workbenchTestServices'; +import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; import { IWorkspaceContextService, Workspace, toWorkspaceFolders } from 'vs/platform/workspace/common/workspace'; import * as uuid from 'vs/base/common/uuid'; import { ConfigurationService } from 'vs/platform/configuration/node/configurationService'; -import { FileService } from 'vs/workbench/services/files/node/fileService'; +import { FileService } from 'vs/workbench/services/files/electron-browser/fileService'; import { IFileService } from 'vs/platform/files/common/files'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IUntitledEditorService, UntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; @@ -80,7 +81,16 @@ suite('Keybindings Editing', () => { instantiationService.stub(ITelemetryService, NullTelemetryService); instantiationService.stub(IModeService, ModeServiceImpl); instantiationService.stub(IModelService, instantiationService.createInstance(ModelServiceImpl)); - instantiationService.stub(IFileService, new FileService(new TestContextService(new Workspace(testDir, testDir, toWorkspaceFolders([{ path: testDir }]))), TestEnvironmentService, new TestTextResourceConfigurationService(), new TestConfigurationService(), lifecycleService, { disableWatcher: true })); + instantiationService.stub(IFileService, new FileService( + new TestContextService(new Workspace(testDir, testDir, toWorkspaceFolders([{ path: testDir }]))), + TestEnvironmentService, + new TestTextResourceConfigurationService(), + new TestConfigurationService(), + lifecycleService, + new TestStorageService(), + new TestNotificationService(), + { disableWatcher: true }) + ); instantiationService.stub(IUntitledEditorService, instantiationService.createInstance(UntitledEditorService)); instantiationService.stub(ITextFileService, instantiationService.createInstance(TestTextFileService)); instantiationService.stub(ITextModelService, instantiationService.createInstance(TextModelResolverService)); diff --git a/src/vs/workbench/services/notification/common/notificationService.ts b/src/vs/workbench/services/notification/common/notificationService.ts index 815873ed0ff..52fbfe0638c 100644 --- a/src/vs/workbench/services/notification/common/notificationService.ts +++ b/src/vs/workbench/services/notification/common/notificationService.ts @@ -5,7 +5,7 @@ 'use strict'; -import { INotificationService, INotification, INotificationHandle, Severity, NotificationMessage, PromptOption, INotificationActions } from 'vs/platform/notification/common/notification'; +import { INotificationService, INotification, INotificationHandle, Severity, NotificationMessage, INotificationActions, IPromptChoice } from 'vs/platform/notification/common/notification'; import { INotificationsModel, NotificationsModel } from 'vs/workbench/common/notifications'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { TPromise } from 'vs/base/common/winjs.base'; @@ -65,61 +65,49 @@ export class NotificationService implements INotificationService { return this.model.notify(notification); } - public prompt(severity: Severity, message: string, choices: PromptOption[]): TPromise { + public prompt(severity: Severity, message: string, choices: IPromptChoice[], onCancel?: () => void): INotificationHandle { let handle: INotificationHandle; + let choiceClicked = false; - const promise = new TPromise(c => { + // Convert choices into primary/secondary actions + const actions: INotificationActions = { primary: [], secondary: [] }; + choices.forEach((choice, index) => { + const action = new Action(`workbench.dialog.choice.${index}`, choice.label, null, true, () => { + choiceClicked = true; - // Complete promise with index of action that was picked - const callback = (index: number, closeNotification: boolean) => () => { - c(index); + // Pass to runner + choice.run(); - if (closeNotification) { - handle.dispose(); + // Close notification unless we are told to keep open + if (!choice.keepOpen) { + handle.close(); } return TPromise.as(void 0); - }; - - // Convert choices into primary/secondary actions - const actions: INotificationActions = { - primary: [], - secondary: [] - }; - - choices.forEach((choice, index) => { - let isPrimary = true; - let label: string; - let closeNotification = false; - - if (typeof choice === 'string') { - label = choice; - } else { - isPrimary = false; - label = choice.label; - closeNotification = !choice.keepOpen; - } - - const action = new Action(`workbench.dialog.choice.${index}`, label, null, true, callback(index, closeNotification)); - if (isPrimary) { - actions.primary.push(action); - } else { - actions.secondary.push(action); - } }); - // Show notification with actions - handle = this.notify({ severity, message, actions }); + if (!choice.isSecondary) { + actions.primary.push(action); + } else { + actions.secondary.push(action); + } + }); - // Cancel promise and cleanup when notification gets disposed - once(handle.onDidDispose)(() => { - dispose(...actions.primary, ...actions.secondary); - promise.cancel(); - }); + // Show notification with actions + handle = this.notify({ severity, message, actions }); - }, () => handle.dispose()); + once(handle.onDidClose)(() => { - return promise; + // Cleanup when notification gets disposed + dispose(...actions.primary, ...actions.secondary); + + // Indicate cancellation to the outside if no action was executed + if (!choiceClicked && typeof onCancel === 'function') { + onCancel(); + } + }); + + return handle; } public dispose(): void { diff --git a/src/vs/workbench/services/part/common/partService.ts b/src/vs/workbench/services/part/common/partService.ts index c9a780fb3b4..c96d2e0ed70 100644 --- a/src/vs/workbench/services/part/common/partService.ts +++ b/src/vs/workbench/services/part/common/partService.ts @@ -28,7 +28,7 @@ export interface ILayoutOptions { source?: Parts; } -export interface Dimension { +export interface IDimension { readonly width: number; readonly height: number; } @@ -46,7 +46,7 @@ export interface IPartService { /** * Emits when the editor part's layout changes. */ - onEditorLayout: Event; + onEditorLayout: Event; /** * Asks the part service to layout all parts. diff --git a/src/vs/workbench/parts/preferences/browser/preferencesService.ts b/src/vs/workbench/services/preferences/browser/preferencesService.ts similarity index 73% rename from src/vs/workbench/parts/preferences/browser/preferencesService.ts rename to src/vs/workbench/services/preferences/browser/preferencesService.ts index c569308d4e8..c9e793a29d6 100644 --- a/src/vs/workbench/parts/preferences/browser/preferencesService.ts +++ b/src/vs/workbench/services/preferences/browser/preferencesService.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import 'vs/css!./media/preferences'; import * as network from 'vs/base/common/network'; import { TPromise } from 'vs/base/common/winjs.base'; import * as nls from 'vs/nls'; @@ -22,11 +21,10 @@ import { IEditorGroupService } from 'vs/workbench/services/group/common/groupSer import { IFileService, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { IPreferencesService, IPreferencesEditorModel, ISetting, getSettingsTargetName, FOLDER_SETTINGS_PATH, DEFAULT_SETTINGS_EDITOR_SETTING } from 'vs/workbench/parts/preferences/common/preferences'; -import { SettingsEditorModel, DefaultSettingsEditorModel, DefaultKeybindingsEditorModel, defaultKeybindingsContents, DefaultSettings, WorkspaceConfigurationEditorModel } from 'vs/workbench/parts/preferences/common/preferencesModels'; +import { IPreferencesService, IPreferencesEditorModel, ISetting, getSettingsTargetName, FOLDER_SETTINGS_PATH, DEFAULT_SETTINGS_EDITOR_SETTING } from 'vs/workbench/services/preferences/common/preferences'; +import { SettingsEditorModel, DefaultSettingsEditorModel, DefaultKeybindingsEditorModel, defaultKeybindingsContents, DefaultSettings, WorkspaceConfigurationEditorModel } from 'vs/workbench/services/preferences/common/preferencesModels'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { DefaultPreferencesEditorInput, PreferencesEditorInput } from 'vs/workbench/parts/preferences/browser/preferencesEditor'; -import { KeybindingsEditorInput } from 'vs/workbench/parts/preferences/browser/keybindingsEditor'; +import { DefaultPreferencesEditorInput, PreferencesEditorInput, KeybindingsEditorInput } from 'vs/workbench/services/preferences/common/preferencesEditorInput'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { getCodeEditor } from 'vs/editor/browser/services/codeEditorService'; import { EditOperation } from 'vs/editor/common/core/editOperation'; @@ -34,7 +32,6 @@ import { Position, IPosition } from 'vs/editor/common/core/position'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IModelService } from 'vs/editor/common/services/modelService'; import { IJSONEditingService } from 'vs/workbench/services/configuration/common/jsonEditing'; -import { ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; import { IModeService } from 'vs/editor/common/services/modeService'; import { parse } from 'vs/base/common/json'; @@ -51,10 +48,12 @@ export class PreferencesService extends Disposable implements IPreferencesServic private readonly _onDispose: Emitter = new Emitter(); - private _defaultSettingsUriCounter = 0; - private _defaultSettingsContentModel: DefaultSettings; - private _defaultResourceSettingsUriCounter = 0; - private _defaultResourceSettingsContentModel: DefaultSettings; + private _defaultUserSettingsUriCounter = 0; + private _defaultUserSettingsContentModel: DefaultSettings; + private _defaultWorkspaceSettingsUriCounter = 0; + private _defaultWorkspaceSettingsContentModel: DefaultSettings; + private _defaultFolderSettingsUriCounter = 0; + private _defaultFolderSettingsContentModel: DefaultSettings; constructor( @IWorkbenchEditorService private editorService: IWorkbenchEditorService, @@ -73,13 +72,6 @@ export class PreferencesService extends Disposable implements IPreferencesServic @IModeService private modeService: IModeService ) { super(); - this.editorGroupService.onEditorsChanged(() => { - const activeEditorInput = this.editorService.getActiveEditorInput(); - if (activeEditorInput instanceof PreferencesEditorInput) { - this.lastOpenedSettingsInput = activeEditorInput; - } - }); - // The default keybindings.json updates based on keyboard layouts, so here we make sure // if a model has been given out we update it accordingly. keybindingService.onDidUpdateKeybindings(() => { @@ -108,9 +100,9 @@ export class PreferencesService extends Disposable implements IPreferencesServic } resolveModel(uri: URI): TPromise { - if (this.isDefaultSettingsResource(uri) || this.isDefaultResourceSettingsResource(uri)) { + if (this.isDefaultSettingsResource(uri)) { - const scope = this.isDefaultSettingsResource(uri) ? ConfigurationScope.WINDOW : ConfigurationScope.RESOURCE; + const target = this.getConfigurationTargetFromDefaultSettingsResource(uri); const mode = this.modeService.getOrCreateMode('jsonc'); const model = this._register(this.modelService.createModel('', mode, uri)); @@ -122,7 +114,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic // model has not been given out => nothing to do return; } - defaultSettings = this.getDefaultSettings(scope); + defaultSettings = this.getDefaultSettings(target); this.modelService.updateModel(model, defaultSettings.parse()); defaultSettings._onDidChange.fire(); } @@ -130,7 +122,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic // Check if Default settings is already created and updated in above promise if (!defaultSettings) { - defaultSettings = this.getDefaultSettings(scope); + defaultSettings = this.getDefaultSettings(target); this.modelService.updateModel(model, defaultSettings.parse()); } @@ -138,7 +130,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic } if (this.defaultSettingsRawResource.toString() === uri.toString()) { - let defaultSettings: DefaultSettings = this.getDefaultSettings(ConfigurationScope.WINDOW); + let defaultSettings: DefaultSettings = this.getDefaultSettings(ConfigurationTarget.USER); const mode = this.modeService.getOrCreateMode('jsonc'); const model = this._register(this.modelService.createModel(defaultSettings.raw, mode, uri)); return TPromise.as(model); @@ -155,7 +147,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic } createPreferencesEditorModel(uri: URI): TPromise> { - if (this.isDefaultSettingsResource(uri) || this.isDefaultResourceSettingsResource(uri)) { + if (this.isDefaultSettingsResource(uri)) { return this.createDefaultSettingsEditorModel(uri); } @@ -179,8 +171,15 @@ export class PreferencesService extends Disposable implements IPreferencesServic return this.editorService.openEditor({ resource: this.defaultSettingsRawResource }, EditorPosition.ONE) as TPromise; } + openSettings(): TPromise { + const editorInput = this.getActiveSettingsEditorInput() || this.lastOpenedSettingsInput; + const resource = editorInput ? editorInput.master.getResource() : this.userSettingsResource; + const target = this.getConfigurationTargetFromSettingsResource(resource); + return this.openOrSwitchSettings(target, resource); + } + openGlobalSettings(options?: IEditorOptions, position?: EditorPosition): TPromise { - return this.doOpenSettings(ConfigurationTarget.USER, this.userSettingsResource, options, position); + return this.openOrSwitchSettings(ConfigurationTarget.USER, this.userSettingsResource, options, position); } openWorkspaceSettings(options?: IEditorOptions, position?: EditorPosition): TPromise { @@ -188,29 +187,19 @@ export class PreferencesService extends Disposable implements IPreferencesServic this.notificationService.info(nls.localize('openFolderFirst', "Open a folder first to create workspace settings")); return TPromise.as(null); } - return this.doOpenSettings(ConfigurationTarget.WORKSPACE, this.workspaceSettingsResource, options, position); + return this.openOrSwitchSettings(ConfigurationTarget.WORKSPACE, this.workspaceSettingsResource, options, position); } openFolderSettings(folder: URI, options?: IEditorOptions, position?: EditorPosition): TPromise { - return this.doOpenSettings(ConfigurationTarget.WORKSPACE_FOLDER, this.getEditableSettingsURI(ConfigurationTarget.WORKSPACE_FOLDER, folder), options, position); + return this.openOrSwitchSettings(ConfigurationTarget.WORKSPACE_FOLDER, this.getEditableSettingsURI(ConfigurationTarget.WORKSPACE_FOLDER, folder), options, position); } switchSettings(target: ConfigurationTarget, resource: URI): TPromise { const activeEditor = this.editorService.getActiveEditor(); if (activeEditor && activeEditor.input instanceof PreferencesEditorInput) { - return this.getOrCreateEditableSettingsEditorInput(target, this.getEditableSettingsURI(target, resource)) - .then(toInput => { - const replaceWith = new PreferencesEditorInput(this.getPreferencesEditorInputName(target, resource), toInput.getDescription(), this.instantiationService.createInstance(DefaultPreferencesEditorInput, this.getDefaultSettingsResource(target)), toInput); - return this.editorService.replaceEditors([{ - toReplace: this.lastOpenedSettingsInput, - replaceWith - }], activeEditor.position).then(() => { - this.lastOpenedSettingsInput = replaceWith; - }); - }); + return this.doSwitchSettings(target, resource, activeEditor.input, activeEditor.position).then(() => null); } else { - this.doOpenSettings(target, resource); - return undefined; + return this.doOpenSettings(target, resource).then(() => null); } } @@ -251,6 +240,16 @@ export class PreferencesService extends Disposable implements IPreferencesServic }); } + private openOrSwitchSettings(configurationTarget: ConfigurationTarget, resource: URI, options?: IEditorOptions, position?: EditorPosition): TPromise { + const activeGroup = this.editorGroupService.getStacksModel().activeGroup; + const positionToReplace = position !== void 0 ? position : activeGroup ? this.editorGroupService.getStacksModel().positionOfGroup(activeGroup) : EditorPosition.ONE; + const editorInput = this.getActiveSettingsEditorInput(positionToReplace); + if (editorInput && editorInput.master.getResource().fsPath !== resource.fsPath) { + return this.doSwitchSettings(configurationTarget, resource, editorInput, positionToReplace); + } + return this.doOpenSettings(configurationTarget, resource, options, position); + } + private doOpenSettings(configurationTarget: ConfigurationTarget, resource: URI, options?: IEditorOptions, position?: EditorPosition): TPromise { const openDefaultSettings = !!this.configurationService.getValue(DEFAULT_SETTINGS_EDITOR_SETTING); return this.getOrCreateEditableSettingsEditorInput(configurationTarget, resource) @@ -271,20 +270,72 @@ export class PreferencesService extends Disposable implements IPreferencesServic }); } + private doSwitchSettings(target: ConfigurationTarget, resource: URI, input: PreferencesEditorInput, position?: EditorPosition): TPromise { + return this.getOrCreateEditableSettingsEditorInput(target, this.getEditableSettingsURI(target, resource)) + .then(toInput => { + const replaceWith = new PreferencesEditorInput(this.getPreferencesEditorInputName(target, resource), toInput.getDescription(), this.instantiationService.createInstance(DefaultPreferencesEditorInput, this.getDefaultSettingsResource(target)), toInput); + return this.editorService.replaceEditors([{ + toReplace: input, + replaceWith + }], position).then(editors => { + this.lastOpenedSettingsInput = replaceWith; + return editors[0]; + }); + }); + } + + private getActiveSettingsEditorInput(position?: EditorPosition): PreferencesEditorInput { + const stacksModel = this.editorGroupService.getStacksModel(); + const group = position !== void 0 ? stacksModel.groupAt(position) : stacksModel.activeGroup; + return group && group.getEditors().filter(e => e instanceof PreferencesEditorInput)[0]; + } + + private getConfigurationTargetFromSettingsResource(resource: URI): ConfigurationTarget { + if (this.userSettingsResource.toString() === resource.toString()) { + return ConfigurationTarget.USER; + } + + const workspaceSettingsResource = this.workspaceSettingsResource; + if (workspaceSettingsResource && workspaceSettingsResource.toString() === resource.toString()) { + return ConfigurationTarget.WORKSPACE; + } + + const folder = this.contextService.getWorkspaceFolder(resource); + if (folder) { + return ConfigurationTarget.WORKSPACE_FOLDER; + } + + return ConfigurationTarget.USER; + } + + private getConfigurationTargetFromDefaultSettingsResource(uri: URI) { + return this.isDefaultWorkspaceSettingsResource(uri) ? ConfigurationTarget.WORKSPACE : this.isDefaultFolderSettingsResource(uri) ? ConfigurationTarget.WORKSPACE_FOLDER : ConfigurationTarget.USER; + } + private isDefaultSettingsResource(uri: URI): boolean { + return this.isDefaultUserSettingsResource(uri) || this.isDefaultWorkspaceSettingsResource(uri) || this.isDefaultFolderSettingsResource(uri); + } + + private isDefaultUserSettingsResource(uri: URI): boolean { return uri.authority === 'defaultsettings' && uri.scheme === network.Schemas.vscode && !!uri.path.match(/\/(\d+\/)?settings\.json$/); } - private isDefaultResourceSettingsResource(uri: URI): boolean { + private isDefaultWorkspaceSettingsResource(uri: URI): boolean { + return uri.authority === 'defaultsettings' && uri.scheme === network.Schemas.vscode && !!uri.path.match(/\/(\d+\/)?workspaceSettings\.json$/); + } + + private isDefaultFolderSettingsResource(uri: URI): boolean { return uri.authority === 'defaultsettings' && uri.scheme === network.Schemas.vscode && !!uri.path.match(/\/(\d+\/)?resourceSettings\.json$/); } private getDefaultSettingsResource(configurationTarget: ConfigurationTarget): URI { - if (configurationTarget === ConfigurationTarget.WORKSPACE_FOLDER) { - return URI.from({ scheme: network.Schemas.vscode, authority: 'defaultsettings', path: `/${this._defaultResourceSettingsUriCounter++}/resourceSettings.json` }); - } else { - return URI.from({ scheme: network.Schemas.vscode, authority: 'defaultsettings', path: `/${this._defaultSettingsUriCounter++}/settings.json` }); + switch (configurationTarget) { + case ConfigurationTarget.WORKSPACE: + return URI.from({ scheme: network.Schemas.vscode, authority: 'defaultsettings', path: `/${this._defaultWorkspaceSettingsUriCounter++}/workspaceSettings.json` }); + case ConfigurationTarget.WORKSPACE_FOLDER: + return URI.from({ scheme: network.Schemas.vscode, authority: 'defaultsettings', path: `/${this._defaultFolderSettingsUriCounter++}/resourceSettings.json` }); } + return URI.from({ scheme: network.Schemas.vscode, authority: 'defaultsettings', path: `/${this._defaultUserSettingsUriCounter++}/settings.json` }); } private getPreferencesEditorInputName(target: ConfigurationTarget, resource: URI): string { @@ -314,24 +365,28 @@ export class PreferencesService extends Disposable implements IPreferencesServic private createDefaultSettingsEditorModel(defaultSettingsUri: URI): TPromise { return this.textModelResolverService.createModelReference(defaultSettingsUri) .then(reference => { - const scope = this.isDefaultSettingsResource(defaultSettingsUri) ? ConfigurationScope.WINDOW : ConfigurationScope.RESOURCE; - return this.instantiationService.createInstance(DefaultSettingsEditorModel, defaultSettingsUri, reference, scope, this.getDefaultSettings(scope)); + const target = this.getConfigurationTargetFromDefaultSettingsResource(defaultSettingsUri); + return this.instantiationService.createInstance(DefaultSettingsEditorModel, defaultSettingsUri, reference, this.getDefaultSettings(target)); }); } - private getDefaultSettings(scope: ConfigurationScope): DefaultSettings { - switch (scope) { - case ConfigurationScope.WINDOW: - if (!this._defaultSettingsContentModel) { - this._defaultSettingsContentModel = new DefaultSettings(this.getMostCommonlyUsedSettings(), scope); - } - return this._defaultSettingsContentModel; - case ConfigurationScope.RESOURCE: - if (!this._defaultResourceSettingsContentModel) { - this._defaultResourceSettingsContentModel = new DefaultSettings(this.getMostCommonlyUsedSettings(), scope); - } - return this._defaultResourceSettingsContentModel; + private getDefaultSettings(target: ConfigurationTarget): DefaultSettings { + if (target === ConfigurationTarget.WORKSPACE) { + if (!this._defaultWorkspaceSettingsContentModel) { + this._defaultWorkspaceSettingsContentModel = new DefaultSettings(this.getMostCommonlyUsedSettings(), target); + } + return this._defaultWorkspaceSettingsContentModel; } + if (target === ConfigurationTarget.WORKSPACE_FOLDER) { + if (!this._defaultFolderSettingsContentModel) { + this._defaultFolderSettingsContentModel = new DefaultSettings(this.getMostCommonlyUsedSettings(), target); + } + return this._defaultFolderSettingsContentModel; + } + if (!this._defaultUserSettingsContentModel) { + this._defaultUserSettingsContentModel = new DefaultSettings(this.getMostCommonlyUsedSettings(), target); + } + return this._defaultUserSettingsContentModel; } private getEditableSettingsURI(configurationTarget: ConfigurationTarget, resource?: URI): URI { diff --git a/src/vs/workbench/parts/preferences/common/keybindingsEditorModel.ts b/src/vs/workbench/services/preferences/common/keybindingsEditorModel.ts similarity index 100% rename from src/vs/workbench/parts/preferences/common/keybindingsEditorModel.ts rename to src/vs/workbench/services/preferences/common/keybindingsEditorModel.ts diff --git a/src/vs/workbench/services/preferences/common/preferences.ts b/src/vs/workbench/services/preferences/common/preferences.ts new file mode 100644 index 00000000000..0a4d82056b0 --- /dev/null +++ b/src/vs/workbench/services/preferences/common/preferences.ts @@ -0,0 +1,164 @@ +/*--------------------------------------------------------------------------------------------- + * 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 } from 'vs/platform/instantiation/common/instantiation'; +import { IEditor, Position, IEditorOptions } from 'vs/platform/editor/common/editor'; +import { ITextModel } from 'vs/editor/common/model'; +import { IRange } from 'vs/editor/common/core/range'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { join } from 'vs/base/common/paths'; +import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; +import { Event } from 'vs/base/common/event'; +import { IStringDictionary } from 'vs/base/common/collections'; +import { ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { localize } from 'vs/nls'; + +export interface ISettingsGroup { + id: string; + range: IRange; + title: string; + titleRange: IRange; + sections: ISettingsSection[]; +} + +export interface ISettingsSection { + titleRange?: IRange; + title?: string; + settings: ISetting[]; +} + +export interface ISetting { + range: IRange; + key: string; + keyRange: IRange; + value: any; + valueRange: IRange; + description: string[]; + descriptionRanges: IRange[]; + overrides?: ISetting[]; + overrideOf?: ISetting; +} + +export interface IExtensionSetting extends ISetting { + extensionName: string; + extensionPublisher: string; +} + +export interface ISearchResult { + filterMatches: ISettingMatch[]; + metadata?: IFilterMetadata; +} + +export interface ISearchResultGroup { + id: string; + label: string; + result: ISearchResult; + order: number; +} + +export interface IFilterResult { + query?: string; + filteredGroups: ISettingsGroup[]; + allGroups: ISettingsGroup[]; + matches: IRange[]; + metadata?: IStringDictionary; +} + +export interface ISettingMatch { + setting: ISetting; + matches: IRange[]; + score: number; +} + +export interface IScoredResults { + [key: string]: IRemoteSetting; +} + +export interface IRemoteSetting { + score: number; + key: string; + id: string; + defaultValue: string; + description: string; + packageId: string; + extensionName?: string; + extensionPublisher?: string; +} + +export interface IFilterMetadata { + requestUrl: string; + requestBody: string; + timestamp: number; + duration: number; + scoredResults: IScoredResults; + extensions?: ILocalExtension[]; + + /** The number of requests made, since requests are split by number of filters */ + requestCount?: number; + + /** The name of the server that actually served the request */ + context: string; +} + +export interface IPreferencesEditorModel { + uri: URI; + getPreference(key: string): T; + dispose(): void; +} + +export type IGroupFilter = (group: ISettingsGroup) => boolean; +export type ISettingMatcher = (setting: ISetting, group: ISettingsGroup) => { matches: IRange[], score: number }; + +export interface ISettingsEditorModel extends IPreferencesEditorModel { + readonly onDidChangeGroups: Event; + settingsGroups: ISettingsGroup[]; + filterSettings(filter: string, groupFilter: IGroupFilter, settingMatcher: ISettingMatcher): ISettingMatch[]; + findValueMatches(filter: string, setting: ISetting): IRange[]; + updateResultGroup(id: string, resultGroup: ISearchResultGroup): IFilterResult; +} + +export interface IKeybindingsEditorModel extends IPreferencesEditorModel { +} + +export const IPreferencesService = createDecorator('preferencesService'); + +export interface IPreferencesService { + _serviceBrand: any; + + userSettingsResource: URI; + workspaceSettingsResource: URI; + getFolderSettingsResource(resource: URI): URI; + + resolveModel(uri: URI): TPromise; + createPreferencesEditorModel(uri: URI): TPromise>; + + openRawDefaultSettings(): TPromise; + openSettings(): TPromise; + openGlobalSettings(options?: IEditorOptions, position?: Position): TPromise; + openWorkspaceSettings(options?: IEditorOptions, position?: Position): TPromise; + openFolderSettings(folder: URI, options?: IEditorOptions, position?: Position): TPromise; + switchSettings(target: ConfigurationTarget, resource: URI): TPromise; + openGlobalKeybindingSettings(textual: boolean): TPromise; + + configureSettingsForLanguage(language: string): void; +} + +export function getSettingsTargetName(target: ConfigurationTarget, resource: URI, workspaceContextService: IWorkspaceContextService): string { + switch (target) { + case ConfigurationTarget.USER: + return localize('userSettingsTarget', "User Settings"); + case ConfigurationTarget.WORKSPACE: + return localize('workspaceSettingsTarget', "Workspace Settings"); + case ConfigurationTarget.WORKSPACE_FOLDER: + const folder = workspaceContextService.getWorkspaceFolder(resource); + return folder ? folder.name : ''; + } + return ''; +} + +export const FOLDER_SETTINGS_PATH = join('.vscode', 'settings.json'); +export const DEFAULT_SETTINGS_EDITOR_SETTING = 'workbench.settings.openDefaultSettings'; diff --git a/src/vs/workbench/services/preferences/common/preferencesEditorInput.ts b/src/vs/workbench/services/preferences/common/preferencesEditorInput.ts new file mode 100644 index 00000000000..f8fb62cb73a --- /dev/null +++ b/src/vs/workbench/services/preferences/common/preferencesEditorInput.ts @@ -0,0 +1,83 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { SideBySideEditorInput, EditorInput } from 'vs/workbench/common/editor'; +import { Verbosity } from 'vs/platform/editor/common/editor'; +import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; +import URI from 'vs/base/common/uri'; +import { ITextModelService } from 'vs/editor/common/services/resolverService'; +import { IHashService } from 'vs/workbench/services/hash/common/hashService'; +import { KeybindingsEditorModel } from 'vs/workbench/services/preferences/common/keybindingsEditorModel'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { OS } from 'vs/base/common/platform'; +import { TPromise } from 'vs/base/common/winjs.base'; + +export class PreferencesEditorInput extends SideBySideEditorInput { + public static readonly ID: string = 'workbench.editorinputs.preferencesEditorInput'; + + getTypeId(): string { + return PreferencesEditorInput.ID; + } + + public supportsSplitEditor(): boolean { + return true; + } + + public getTitle(verbosity: Verbosity): string { + return this.master.getTitle(verbosity); + } +} + +export class DefaultPreferencesEditorInput extends ResourceEditorInput { + public static readonly ID = 'workbench.editorinputs.defaultpreferences'; + constructor(defaultSettingsResource: URI, + @ITextModelService textModelResolverService: ITextModelService, + @IHashService hashService: IHashService + ) { + super(nls.localize('settingsEditorName', "Default Settings"), '', defaultSettingsResource, textModelResolverService, hashService); + } + + getTypeId(): string { + return DefaultPreferencesEditorInput.ID; + } + + matches(other: any): boolean { + if (other instanceof DefaultPreferencesEditorInput) { + return true; + } + if (!super.matches(other)) { + return false; + } + return true; + } +} + +export class KeybindingsEditorInput extends EditorInput { + + public static readonly ID: string = 'workbench.input.keybindings'; + public readonly keybindingsModel: KeybindingsEditorModel; + + constructor(@IInstantiationService instantiationService: IInstantiationService) { + super(); + this.keybindingsModel = instantiationService.createInstance(KeybindingsEditorModel, OS); + } + + getTypeId(): string { + return KeybindingsEditorInput.ID; + } + + getName(): string { + return nls.localize('keybindingsInputName', "Keyboard Shortcuts"); + } + + resolve(refresh?: boolean): TPromise { + return TPromise.as(this.keybindingsModel); + } + + matches(otherInput: any): boolean { + return otherInput instanceof KeybindingsEditorInput; + } +} diff --git a/src/vs/workbench/parts/preferences/common/preferencesModels.ts b/src/vs/workbench/services/preferences/common/preferencesModels.ts similarity index 98% rename from src/vs/workbench/parts/preferences/common/preferencesModels.ts rename to src/vs/workbench/services/preferences/common/preferencesModels.ts index 3119e7a4b16..a3ea720f8b6 100644 --- a/src/vs/workbench/parts/preferences/common/preferencesModels.ts +++ b/src/vs/workbench/services/preferences/common/preferencesModels.ts @@ -15,7 +15,7 @@ import { visit, JSONVisitor } from 'vs/base/common/json'; import { ITextModel, IIdentifiedSingleEditOperation } from 'vs/editor/common/model'; import { EditorModel } from 'vs/workbench/common/editor'; import { IConfigurationNode, IConfigurationRegistry, Extensions, OVERRIDE_PROPERTY_PATTERN, IConfigurationPropertySchema, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; -import { ISettingsEditorModel, IKeybindingsEditorModel, ISettingsGroup, ISetting, IFilterResult, IGroupFilter, ISettingMatcher, ISettingMatch, ISearchResultGroup, IFilterMetadata } from 'vs/workbench/parts/preferences/common/preferences'; +import { ISettingsEditorModel, IKeybindingsEditorModel, ISettingsGroup, ISetting, IFilterResult, IGroupFilter, ISettingMatcher, ISettingMatch, ISearchResultGroup, IFilterMetadata } from 'vs/workbench/services/preferences/common/preferences'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ITextEditorModel } from 'vs/editor/common/services/resolverService'; import { IRange, Range } from 'vs/editor/common/core/range'; @@ -403,7 +403,7 @@ export class DefaultSettings extends Disposable { constructor( private _mostCommonlyUsedSettingsKeys: string[], - readonly configurationScope: ConfigurationScope, + readonly target: ConfigurationTarget, ) { super(); } @@ -555,10 +555,13 @@ export class DefaultSettings extends Disposable { } private matchesScope(property: IConfigurationNode): boolean { - if (this.configurationScope === ConfigurationScope.WINDOW) { - return true; + if (this.target === ConfigurationTarget.WORKSPACE_FOLDER) { + return property.scope === ConfigurationScope.RESOURCE; } - return property.scope === this.configurationScope; + if (this.target === ConfigurationTarget.WORKSPACE) { + return property.scope === ConfigurationScope.WINDOW || property.scope === ConfigurationScope.RESOURCE; + } + return true; } private compareConfigurationNodes(c1: IConfigurationNode, c2: IConfigurationNode): number { @@ -603,7 +606,6 @@ export class DefaultSettingsEditorModel extends AbstractSettingsModel implements constructor( private _uri: URI, reference: IReference, - readonly configurationScope: ConfigurationScope, private readonly defaultSettings: DefaultSettings ) { super(); @@ -617,6 +619,10 @@ export class DefaultSettingsEditorModel extends AbstractSettingsModel implements return this._uri; } + public get target(): ConfigurationTarget { + return this.defaultSettings.target; + } + public get settingsGroups(): ISettingsGroup[] { return this.defaultSettings.settingsGroups; } diff --git a/src/vs/workbench/parts/preferences/test/common/keybindingsEditorModel.test.ts b/src/vs/workbench/services/preferences/test/common/keybindingsEditorModel.test.ts similarity index 99% rename from src/vs/workbench/parts/preferences/test/common/keybindingsEditorModel.test.ts rename to src/vs/workbench/services/preferences/test/common/keybindingsEditorModel.test.ts index 195bae94fae..e607fdd1b5f 100644 --- a/src/vs/workbench/parts/preferences/test/common/keybindingsEditorModel.test.ts +++ b/src/vs/workbench/services/preferences/test/common/keybindingsEditorModel.test.ts @@ -16,7 +16,7 @@ import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/wor import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { KeybindingsEditorModel, IKeybindingItemEntry } from 'vs/workbench/parts/preferences/common/keybindingsEditorModel'; +import { KeybindingsEditorModel, IKeybindingItemEntry } from 'vs/workbench/services/preferences/common/keybindingsEditorModel'; import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKeybindingItem'; import { USLayoutResolvedKeybinding } from 'vs/platform/keybinding/common/usLayoutResolvedKeybinding'; diff --git a/src/vs/workbench/services/progress/browser/progressService.ts b/src/vs/workbench/services/progress/browser/progressService.ts index f34a176cfe3..dac37102f25 100644 --- a/src/vs/workbench/services/progress/browser/progressService.ts +++ b/src/vs/workbench/services/progress/browser/progressService.ts @@ -102,17 +102,17 @@ export class WorkbenchProgressService extends ScopedService implements IProgress // Replay Infinite Progress else if (this.progressState.infinite) { - this.progressbar.infinite().getContainer().show(); + this.progressbar.infinite().show(); } // Replay Finite Progress (Total & Worked) else { if (this.progressState.total) { - this.progressbar.total(this.progressState.total).getContainer().show(); + this.progressbar.total(this.progressState.total).show(); } if (this.progressState.worked) { - this.progressbar.worked(this.progressState.worked).getContainer().show(); + this.progressbar.worked(this.progressState.worked).show(); } } } @@ -152,20 +152,12 @@ export class WorkbenchProgressService extends ScopedService implements IProgress // Infinite: Start Progressbar and Show after Delay if (!types.isUndefinedOrNull(infinite)) { - if (types.isUndefinedOrNull(delay)) { - this.progressbar.infinite().getContainer().show(); - } else { - this.progressbar.infinite().getContainer().showDelayed(delay); - } + this.progressbar.infinite().show(delay); } // Finite: Start Progressbar and Show after Delay else if (!types.isUndefinedOrNull(total)) { - if (types.isUndefinedOrNull(delay)) { - this.progressbar.total(total).getContainer().show(); - } else { - this.progressbar.total(total).getContainer().showDelayed(delay); - } + this.progressbar.total(total).show(delay); } } @@ -200,7 +192,7 @@ export class WorkbenchProgressService extends ScopedService implements IProgress this.progressState.infinite = true; this.progressState.worked = void 0; this.progressState.total = void 0; - this.progressbar.infinite().getContainer().show(); + this.progressbar.infinite().show(); } }, @@ -209,7 +201,7 @@ export class WorkbenchProgressService extends ScopedService implements IProgress this.progressState.done = true; if (this.isActive) { - this.progressbar.stop().getContainer().hide(); + this.progressbar.stop().hide(); } } }; @@ -244,7 +236,7 @@ export class WorkbenchProgressService extends ScopedService implements IProgress this.clearProgressState(); if (this.isActive) { - this.progressbar.stop().getContainer().hide(); + this.progressbar.stop().hide(); } }; @@ -257,11 +249,7 @@ export class WorkbenchProgressService extends ScopedService implements IProgress // Show Progress when active if (this.isActive) { - if (types.isUndefinedOrNull(delay)) { - this.progressbar.infinite().getContainer().show(); - } else { - this.progressbar.infinite().getContainer().showDelayed(delay); - } + this.progressbar.infinite().show(delay); } } diff --git a/src/vs/workbench/services/progress/browser/progressService2.ts b/src/vs/workbench/services/progress/browser/progressService2.ts index 9359d3e0623..876dd549910 100644 --- a/src/vs/workbench/services/progress/browser/progressService2.ts +++ b/src/vs/workbench/services/progress/browser/progressService2.ts @@ -166,10 +166,10 @@ export class ProgressService2 implements IProgressService2 { } } - private _withNotificationProgress

    , R=any>(options: IProgressOptions, callback: (progress: IProgress<{ message?: string, percentage?: number }>) => P, onDidCancel?: () => void): P { + private _withNotificationProgress

    , R=any>(options: IProgressOptions, callback: (progress: IProgress<{ message?: string, increment?: number }>) => P, onDidCancel?: () => void): P { const toDispose: IDisposable[] = []; - const createNotification = (message: string, percentage?: number): INotificationHandle => { + const createNotification = (message: string, increment?: number): INotificationHandle => { if (!message) { return undefined; // we need a message at least } @@ -201,35 +201,35 @@ export class ProgressService2 implements IProgressService2 { actions }); - updateProgress(handle, percentage); + updateProgress(handle, increment); - once(handle.onDidDispose)(() => { + once(handle.onDidClose)(() => { dispose(toDispose); }); return handle; }; - const updateProgress = (notification: INotificationHandle, percentage?: number): void => { - if (typeof percentage === 'number' && percentage > 0) { + const updateProgress = (notification: INotificationHandle, increment?: number): void => { + if (typeof increment === 'number' && increment >= 0) { notification.progress.total(100); // always percentage based - notification.progress.worked(percentage); + notification.progress.worked(increment); } else { notification.progress.infinite(); } }; let handle: INotificationHandle; - const updateNotification = (message?: string, percentage?: number): void => { + const updateNotification = (message?: string, increment?: number): void => { if (!handle) { - handle = createNotification(message, percentage); + handle = createNotification(message, increment); } else { if (typeof message === 'string') { handle.updateMessage(message); } - if (typeof percentage === 'number') { - updateProgress(handle, percentage); + if (typeof increment === 'number') { + updateProgress(handle, increment); } } }; @@ -240,14 +240,14 @@ export class ProgressService2 implements IProgressService2 { // Update based on progress const p = callback({ report: progress => { - updateNotification(progress.message, progress.percentage); + updateNotification(progress.message, progress.increment); } }); // Show progress for at least 800ms and then hide once done or canceled always(TPromise.join([TPromise.timeout(800), p]), () => { if (handle) { - handle.dispose(); + handle.close(); } }); diff --git a/src/vs/workbench/services/progress/test/progressService.test.ts b/src/vs/workbench/services/progress/test/progressService.test.ts index 0f8a924c3fd..577d37ad5b3 100644 --- a/src/vs/workbench/services/progress/test/progressService.test.ts +++ b/src/vs/workbench/services/progress/test/progressService.test.ts @@ -22,10 +22,12 @@ let activeViewlet: Viewlet = {} as any; class TestViewletService implements IViewletService { public _serviceBrand: any; + onDidViewletRegisterEmitter = new Emitter(); onDidViewletOpenEmitter = new Emitter(); onDidViewletCloseEmitter = new Emitter(); onDidViewletEnableEmitter = new Emitter<{ id: string, enabled: boolean }>(); + onDidViewletRegister = this.onDidViewletRegisterEmitter.event; onDidViewletOpen = this.onDidViewletOpenEmitter.event; onDidViewletClose = this.onDidViewletCloseEmitter.event; onDidViewletEnablementChange = this.onDidViewletEnableEmitter.event; @@ -212,11 +214,12 @@ class TestProgressBar { return this.done(); } - public getContainer() { - return { - show: function () { }, - hide: function () { } - }; + public show(): void { + + } + + public hide(): void { + } } diff --git a/src/vs/workbench/services/search/node/ripgrepTextSearch.ts b/src/vs/workbench/services/search/node/ripgrepTextSearch.ts index 57cb8d2bc06..8f2f0e3c850 100644 --- a/src/vs/workbench/services/search/node/ripgrepTextSearch.ts +++ b/src/vs/workbench/services/search/node/ripgrepTextSearch.ts @@ -29,7 +29,7 @@ const rgDiskPath = rgPath.replace(/\bnode_modules\.asar\b/, 'node_modules.asar.u export class RipgrepEngine { private isDone = false; private rgProc: cp.ChildProcess; - private killRgProcFn: Function; + private killRgProcFn: (code?: number) => void; private postProcessExclusions: glob.ParsedExpression; private ripgrepParser: RipgrepParser; diff --git a/src/vs/workbench/services/search/node/worker/searchWorker.ts b/src/vs/workbench/services/search/node/worker/searchWorker.ts index a9490a8e459..cdda1b2b257 100644 --- a/src/vs/workbench/services/search/node/worker/searchWorker.ts +++ b/src/vs/workbench/services/search/node/worker/searchWorker.ts @@ -13,9 +13,7 @@ import { onUnexpectedError } from 'vs/base/common/errors'; import * as strings from 'vs/base/common/strings'; import { TPromise } from 'vs/base/common/winjs.base'; import { LineMatch, FileMatch } from '../search'; -import * as baseMime from 'vs/base/common/mime'; -import { UTF16le, UTF16be, UTF8, UTF8_with_bom, encodingExists, decode, bomLength } from 'vs/base/node/encoding'; -import { detectMimeAndEncodingFromBuffer } from 'vs/base/node/mime'; +import { UTF16le, UTF16be, UTF8, UTF8_with_bom, encodingExists, decode, bomLength, detectEncodingFromBuffer } from 'vs/base/node/encoding'; import { ISearchWorker, ISearchWorkerSearchArgs, ISearchWorkerSearchResult } from './searchWorkerIpc'; @@ -208,13 +206,13 @@ export class SearchWorkerEngine { // Detect encoding and mime when this is the beginning of the file if (isFirstRead) { - const mimeAndEncoding = detectMimeAndEncodingFromBuffer({ buffer, bytesRead }, false); - if (mimeAndEncoding.mimes[mimeAndEncoding.mimes.length - 1] !== baseMime.MIME_TEXT) { + const detected = detectEncodingFromBuffer({ buffer, bytesRead }, false); + if (detected.seemsBinary) { return clb(null); // skip files that seem binary } // Check for BOM offset - switch (mimeAndEncoding.encoding) { + switch (detected.encoding) { case UTF8: pos = i = bomLength(UTF8); options.encoding = UTF8; diff --git a/src/vs/workbench/services/search/test/node/searchService.test.ts b/src/vs/workbench/services/search/test/node/searchService.test.ts index c687527d3b3..5d33088e881 100644 --- a/src/vs/workbench/services/search/test/node/searchService.test.ts +++ b/src/vs/workbench/services/search/test/node/searchService.test.ts @@ -109,7 +109,7 @@ suite('SearchService', () => { assert.deepStrictEqual(value, match); results++; } else { - assert.fail(value); + assert.fail(JSON.stringify(value)); } }); }); @@ -131,7 +131,7 @@ suite('SearchService', () => { }); results.push(value.length); } else { - assert.fail(value); + assert.fail(JSON.stringify(value)); } }); }); @@ -218,7 +218,7 @@ suite('SearchService', () => { if (Array.isArray(value)) { results.push(...value.map(v => v.path)); } else { - assert.fail(value); + assert.fail(JSON.stringify(value)); } }); }); @@ -245,7 +245,7 @@ suite('SearchService', () => { }); results.push(value.length); } else { - assert.fail(value); + assert.fail(JSON.stringify(value)); } }); }); @@ -275,7 +275,7 @@ suite('SearchService', () => { if (Array.isArray(value)) { results.push(...value.map(v => v.path)); } else { - assert.fail(value); + assert.fail(JSON.stringify(value)); } }).then(() => { const results = []; @@ -291,7 +291,7 @@ suite('SearchService', () => { if (Array.isArray(value)) { results.push(...value.map(v => v.path)); } else { - assert.fail(value); + assert.fail(JSON.stringify(value)); } }); }).then(() => { @@ -316,7 +316,7 @@ suite('SearchService', () => { if (Array.isArray(value)) { results.push(...value.map(v => v.path)); } else { - assert.fail(value); + assert.fail(JSON.stringify(value)); } }); }); diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts index 2dc9ec326c5..6b86202d1a1 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts @@ -18,7 +18,7 @@ import * as types from 'vs/base/common/types'; import { IMode } from 'vs/editor/common/modes'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { ITextFileService, IAutoSaveConfiguration, ModelState, ITextFileEditorModel, ISaveOptions, ISaveErrorHandler, ISaveParticipant, StateChange, SaveReason, IRawTextContent } from 'vs/workbench/services/textfile/common/textfiles'; +import { ITextFileService, IAutoSaveConfiguration, ModelState, ITextFileEditorModel, ISaveOptions, ISaveErrorHandler, ISaveParticipant, StateChange, SaveReason, IRawTextContent, ILoadOptions } from 'vs/workbench/services/textfile/common/textfiles'; import { EncodingMode } from 'vs/workbench/common/editor'; import { BaseTextEditorModel } from 'vs/workbench/common/editor/textEditorModel'; import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; @@ -128,38 +128,51 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } private onFileChanges(e: FileChangesEvent): void { + let fileEventImpactsModel = false; + let newInOrphanModeGuess: boolean; - // Track ADD and DELETES for updates of this model to orphan-mode - const modelFileDeleted = e.contains(this.resource, FileChangeType.DELETED); - const modelFileAdded = e.contains(this.resource, FileChangeType.ADDED); - - if (modelFileDeleted || modelFileAdded) { - const newInOrphanModeGuess = modelFileDeleted && !modelFileAdded; - if (this.inOrphanMode !== newInOrphanModeGuess) { - let checkOrphanedPromise: TPromise; - if (newInOrphanModeGuess) { - // We have received reports of users seeing delete events even though the file still - // exists (network shares issue: https://github.com/Microsoft/vscode/issues/13665). - // Since we do not want to mark the model as orphaned, we have to check if the - // file is really gone and not just a faulty file event. - checkOrphanedPromise = TPromise.timeout(100).then(() => { - if (this.disposed) { - return true; - } - - return this.fileService.existsFile(this.resource).then(exists => !exists); - }); - } else { - checkOrphanedPromise = TPromise.as(false); - } - - checkOrphanedPromise.done(newInOrphanModeValidated => { - if (this.inOrphanMode !== newInOrphanModeValidated && !this.disposed) { - this.setOrphaned(newInOrphanModeValidated); - } - }); + // If we are currently orphaned, we check if the model file was added back + if (this.inOrphanMode) { + const modelFileAdded = e.contains(this.resource, FileChangeType.ADDED); + if (modelFileAdded) { + newInOrphanModeGuess = false; + fileEventImpactsModel = true; } } + + // Otherwise we check if the model file was deleted + else { + const modelFileDeleted = e.contains(this.resource, FileChangeType.DELETED); + if (modelFileDeleted) { + newInOrphanModeGuess = true; + fileEventImpactsModel = true; + } + } + + if (fileEventImpactsModel && this.inOrphanMode !== newInOrphanModeGuess) { + let checkOrphanedPromise: TPromise; + if (newInOrphanModeGuess) { + // We have received reports of users seeing delete events even though the file still + // exists (network shares issue: https://github.com/Microsoft/vscode/issues/13665). + // Since we do not want to mark the model as orphaned, we have to check if the + // file is really gone and not just a faulty file event. + checkOrphanedPromise = TPromise.timeout(100).then(() => { + if (this.disposed) { + return true; + } + + return this.fileService.existsFile(this.resource).then(exists => !exists); + }); + } else { + checkOrphanedPromise = TPromise.as(false); + } + + checkOrphanedPromise.done(newInOrphanModeValidated => { + if (this.inOrphanMode !== newInOrphanModeValidated && !this.disposed) { + this.setOrphaned(newInOrphanModeValidated); + } + }); + } } private setOrphaned(orphaned: boolean): void { @@ -243,7 +256,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil if (soft) { loadPromise = TPromise.as(this); } else { - loadPromise = this.load(true /* force */); + loadPromise = this.load({ forceReadFromDisk: true }); } return loadPromise.then(() => { @@ -259,28 +272,28 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil }); } - public load(force?: boolean /* bypass any caches and really go to disk */): TPromise { + public load(options?: ILoadOptions): TPromise { diag('load() - enter', this.resource, new Date()); // It is very important to not reload the model when the model is dirty. We only want to reload the model from the disk // if no save is pending to avoid data loss. This might cause a save conflict in case the file has been modified on the disk // meanwhile, but this is a very low risk. - if (this.dirty) { - diag('load() - exit - without loading because model is dirty', this.resource, new Date()); + if (this.dirty || this.saveSequentializer.hasPendingSave()) { + diag('load() - exit - without loading because model is dirty or being saved', this.resource, new Date()); return TPromise.as(this); } // Only for new models we support to load from backup if (!this.textEditorModel && !this.createTextEditorModelPromise) { - return this.loadWithBackup(force); + return this.loadWithBackup(options); } // Otherwise load from file resource - return this.loadFromFile(force); + return this.loadFromFile(options); } - private loadWithBackup(force: boolean): TPromise { + private loadWithBackup(options?: ILoadOptions): TPromise { return this.backupFileService.loadBackupResource(this.resource).then(backup => { // Make sure meanwhile someone else did not suceed or start loading @@ -296,30 +309,32 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil mtime: Date.now(), etag: void 0, value: createTextBufferFactory(''), /* will be filled later from backup */ - encoding: this.fileService.getEncoding(this.resource, this.preferredEncoding) + encoding: this.fileService.encoding.getWriteEncoding(this.resource, this.preferredEncoding) }; return this.loadWithContent(content, backup); } // Otherwise load from file - return this.loadFromFile(force); + return this.loadFromFile(options); }); } - private loadFromFile(force: boolean): TPromise { + private loadFromFile(options?: ILoadOptions): TPromise { + const forceReadFromDisk = options && options.forceReadFromDisk; + const allowBinary = this.isResolved() /* always allow if we resolved previously */ || (options && options.allowBinary); // Decide on etag let etag: string; - if (force) { - etag = void 0; // bypass cache if force loading is true + if (forceReadFromDisk) { + etag = void 0; // reset ETag if we enforce to read from disk } else if (this.lastResolvedDiskStat) { etag = this.lastResolvedDiskStat.etag; // otherwise respect etag to support caching } // Resolve Content return this.textFileService - .resolveTextContent(this.resource, { acceptTextOnly: true, etag, encoding: this.preferredEncoding }) + .resolveTextContent(this.resource, { acceptTextOnly: !allowBinary, etag, encoding: this.preferredEncoding }) .then(content => this.handleLoadSuccess(content), error => this.handleLoadError(error)); } @@ -677,16 +692,15 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // mark the save participant as current pending save operation return this.saveSequentializer.setPending(versionId, saveParticipantPromise.then(newVersionId => { - // Under certain conditions a save to the model will not cause the contents to the flushed on - // disk because we can assume that the contents are already on disk. Instead, we just touch the - // file to still trigger external file watchers for example. + // Under certain conditions we do a short-cut of flushing contents to disk when we can assume that + // the file has not changed and as such was not dirty before. // The conditions are all of: // - a forced, explicit save (Ctrl+S) // - the model is not dirty (otherwise we know there are changed which needs to go to the file) // - the model is not in orphan mode (because in that case we know the file does not exist on disk) // - the model version did not change due to save participants running if (options.force && !this.dirty && !this.inOrphanMode && options.reason === SaveReason.EXPLICIT && versionId === newVersionId) { - return this.doTouch(); + return this.doTouch(newVersionId); } // update versionId with its new value (if pre-save changes happened) @@ -776,12 +790,16 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil }); } - private doTouch(): TPromise { - return this.fileService.touchFile(this.resource).then(stat => { + private doTouch(versionId: number): TPromise { + return this.saveSequentializer.setPending(versionId, this.fileService.updateContent(this.lastResolvedDiskStat.resource, this.createSnapshot(), { + mtime: this.lastResolvedDiskStat.mtime, + encoding: this.getEncoding(), + etag: this.lastResolvedDiskStat.etag + }).then(stat => { // Updated resolved stat with updated stat since touching it might have changed mtime this.updateLastResolvedDiskStat(stat); - }, () => void 0 /* gracefully ignore errors if just touching */); + }, () => void 0 /* gracefully ignore errors if just touching */)); } private setDirty(dirty: boolean): () => void { @@ -827,8 +845,8 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } // Subsequent resolve - make sure that we only assign it if the mtime is equal or has advanced. - // This is essential a If-Modified-Since check on the client ot prevent race conditions from loading - // and saving. If a save comes in late after a revert was called, the mtime could be out of sync. + // This prevents race conditions from loading and saving. If a save comes in late after a revert + // was called, the mtime could be out of sync. else if (this.lastResolvedDiskStat.mtime <= newVersionOnDiskStat.mtime) { this.lastResolvedDiskStat = newVersionOnDiskStat; } @@ -921,7 +939,9 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil this.updatePreferredEncoding(encoding); // Load - this.load(true /* force because encoding has changed */).done(null, onUnexpectedError); + this.load({ + forceReadFromDisk: true // because encoding has changed + }).done(null, onUnexpectedError); } } diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts b/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts index c617d2113f8..7b19b9d0a36 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts @@ -9,7 +9,7 @@ import { TPromise } from 'vs/base/common/winjs.base'; import URI from 'vs/base/common/uri'; import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; import { dispose, IDisposable } from 'vs/base/common/lifecycle'; -import { ITextFileEditorModel, ITextFileEditorModelManager, TextFileModelChangeEvent, StateChange, IModelLoadOrCreateOptions } from 'vs/workbench/services/textfile/common/textfiles'; +import { ITextFileEditorModel, ITextFileEditorModelManager, TextFileModelChangeEvent, StateChange, IModelLoadOrCreateOptions, ILoadOptions } from 'vs/workbench/services/textfile/common/textfiles'; import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ResourceMap } from 'vs/base/common/map'; @@ -167,22 +167,27 @@ export class TextFileEditorModelManager implements ITextFileEditorModelManager { return pendingLoad; } + let modelLoadOptions: ILoadOptions; + if (options && options.allowBinary) { + modelLoadOptions = { allowBinary: true }; + } + let modelPromise: TPromise; // Model exists let model = this.get(resource); if (model) { - if (!options || !options.reload) { - modelPromise = TPromise.as(model); + if (options && options.reload) { + modelPromise = model.load(modelLoadOptions); } else { - modelPromise = model.load(); + modelPromise = TPromise.as(model); } } // Model does not exist else { model = this.instantiationService.createInstance(TextFileEditorModel, resource, options ? options.encoding : void 0); - modelPromise = model.load(); + modelPromise = model.load(modelLoadOptions); // Install state change listener this.mapResourceToStateChangeListener.set(resource, model.onDidStateChange(state => { diff --git a/src/vs/workbench/services/textfile/common/textfiles.ts b/src/vs/workbench/services/textfile/common/textfiles.ts index cacab86e342..94d8b44b598 100644 --- a/src/vs/workbench/services/textfile/common/textfiles.ts +++ b/src/vs/workbench/services/textfile/common/textfiles.ts @@ -140,8 +140,22 @@ export interface IRawTextContent extends IBaseStat { } export interface IModelLoadOrCreateOptions { + + + /** + * The encoding to use when resolving the model text content. + */ encoding?: string; + + /** + * Wether to reload the model if it already exists. + */ reload?: boolean; + + /** + * Allow to load a model even if we think it is a binary file. + */ + allowBinary?: boolean; } export interface ITextFileEditorModelManager { @@ -179,6 +193,19 @@ export interface ISaveOptions { writeElevated?: boolean; } +export interface ILoadOptions { + + /** + * Go to disk bypassing any cache of the model if any. + */ + forceReadFromDisk?: boolean; + + /** + * Allow to load a model even if we think it is a binary file. + */ + allowBinary?: boolean; +} + export interface ITextFileEditorModel extends ITextEditorModel, IEncodingSupport { onDidContentChange: Event; @@ -196,7 +223,7 @@ export interface ITextFileEditorModel extends ITextEditorModel, IEncodingSupport save(options?: ISaveOptions): TPromise; - load(): TPromise; + load(options?: ILoadOptions): TPromise; revert(soft?: boolean): TPromise; diff --git a/src/vs/workbench/services/textmodelResolver/common/textModelResolverService.ts b/src/vs/workbench/services/textmodelResolver/common/textModelResolverService.ts index dcb3faa86c3..c7617a56e68 100644 --- a/src/vs/workbench/services/textmodelResolver/common/textModelResolverService.ts +++ b/src/vs/workbench/services/textmodelResolver/common/textModelResolverService.ts @@ -17,6 +17,7 @@ import * as network from 'vs/base/common/network'; import { ITextModelService, ITextModelContentProvider, ITextEditorModel } from 'vs/editor/common/services/resolverService'; import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; +import { IFileService } from 'vs/platform/files/common/files'; class ResourceModelCollection extends ReferenceCollection> { @@ -24,19 +25,15 @@ class ResourceModelCollection extends ReferenceCollection { const resource = URI.parse(key); - - if (resource.scheme === network.Schemas.file) { - return this.textFileService.models.loadOrCreate(resource); - } - if (!this.providers[resource.scheme]) { - // TODO@remote + if (this.fileService.canHandleResource(resource)) { return this.textFileService.models.loadOrCreate(resource); } return this.resolveTextModelContent(key).then(() => this.instantiationService.createInstance(ResourceEditorModel, resource)); diff --git a/src/vs/workbench/services/themes/electron-browser/themeCompatibility.ts b/src/vs/workbench/services/themes/electron-browser/themeCompatibility.ts index a05bfc5cf4f..7ce9567369d 100644 --- a/src/vs/workbench/services/themes/electron-browser/themeCompatibility.ts +++ b/src/vs/workbench/services/themes/electron-browser/themeCompatibility.ts @@ -66,6 +66,7 @@ addSettingMapping('rangeHighlight', editorColorRegistry.editorRangeHighlight); addSettingMapping('caret', editorColorRegistry.editorCursorForeground); addSettingMapping('invisibles', editorColorRegistry.editorWhitespaces); addSettingMapping('guide', editorColorRegistry.editorIndentGuides); +addSettingMapping('activeGuide', editorColorRegistry.editorActiveIndentGuides); const ansiColorMap = ['ansiBlack', 'ansiRed', 'ansiGreen', 'ansiYellow', 'ansiBlue', 'ansiMagenta', 'ansiCyan', 'ansiWhite', 'ansiBrightBlack', 'ansiBrightRed', 'ansiBrightGreen', 'ansiBrightYellow', 'ansiBrightBlue', 'ansiBrightMagenta', 'ansiBrightCyan', 'ansiBrightWhite' diff --git a/src/vs/workbench/services/themes/electron-browser/workbenchThemeService.ts b/src/vs/workbench/services/themes/electron-browser/workbenchThemeService.ts index 238a245d3ce..abe12f5b9e7 100644 --- a/src/vs/workbench/services/themes/electron-browser/workbenchThemeService.ts +++ b/src/vs/workbench/services/themes/electron-browser/workbenchThemeService.ts @@ -20,10 +20,7 @@ import { ColorThemeData } from './colorThemeData'; import { ITheme, Extensions as ThemingExtensions, IThemingRegistry } from 'vs/platform/theme/common/themeService'; import { editorBackground } from 'vs/platform/theme/common/colorRegistry'; import { Color } from 'vs/base/common/color'; - -import { $ } from 'vs/base/browser/builder'; import { Event, Emitter } from 'vs/base/common/event'; - import * as colorThemeSchema from 'vs/workbench/services/themes/common/colorThemeSchema'; import * as fileIconThemeSchema from 'vs/workbench/services/themes/common/fileIconThemeSchema'; import { IDisposable } from 'vs/base/common/lifecycle'; @@ -32,6 +29,7 @@ import { ColorThemeStore } from 'vs/workbench/services/themes/electron-browser/c import { FileIconThemeStore } from 'vs/workbench/services/themes/electron-browser/fileIconThemeStore'; import { FileIconThemeData } from 'vs/workbench/services/themes/electron-browser/fileIconThemeData'; import { IWindowService } from 'vs/platform/windows/common/windows'; +import { removeClasses, addClasses } from 'vs/base/browser/dom'; // implementation @@ -315,11 +313,11 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { private applyTheme(newTheme: ColorThemeData, settingsTarget: ConfigurationTarget, silent = false): TPromise { if (this.container) { if (this.currentColorTheme) { - $(this.container).removeClass(this.currentColorTheme.id); + removeClasses(this.container, this.currentColorTheme.id); } else { - $(this.container).removeClass(VS_DARK_THEME, VS_LIGHT_THEME, VS_HC_THEME); + removeClasses(this.container, VS_DARK_THEME, VS_LIGHT_THEME, VS_HC_THEME); } - $(this.container).addClass(newTheme.id); + addClasses(this.container, newTheme.id); } this.currentColorTheme = newTheme; if (!this.themingParticipantChangeListener) { @@ -362,7 +360,7 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { "id" : { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" }, "name": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" }, "isBuiltin": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, - "publisherDisplayName": { "classification": "PublicPersonalData", "purpose": "FeatureInsight" }, + "publisherDisplayName": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "themeId": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" } } */ @@ -415,9 +413,9 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { if (this.container) { if (iconThemeData.id) { - $(this.container).addClass(fileIconsEnabledClass); + addClasses(this.container, fileIconsEnabledClass); } else { - $(this.container).removeClass(fileIconsEnabledClass); + removeClasses(this.container, fileIconsEnabledClass); } } if (iconThemeData.id) { diff --git a/src/vs/workbench/services/viewlet/browser/viewlet.ts b/src/vs/workbench/services/viewlet/browser/viewlet.ts index ba1f9f17a5f..02006a2397a 100644 --- a/src/vs/workbench/services/viewlet/browser/viewlet.ts +++ b/src/vs/workbench/services/viewlet/browser/viewlet.ts @@ -16,6 +16,7 @@ export const IViewletService = createDecorator('viewletService' export interface IViewletService { _serviceBrand: ServiceIdentifier; + onDidViewletRegister: Event; onDidViewletOpen: Event; onDidViewletClose: Event; onDidViewletEnablementChange: Event<{ id: string, enabled: boolean }>; diff --git a/src/vs/workbench/services/viewlet/browser/viewletService.ts b/src/vs/workbench/services/viewlet/browser/viewletService.ts index a54cd61ec89..0a5271c8081 100644 --- a/src/vs/workbench/services/viewlet/browser/viewletService.ts +++ b/src/vs/workbench/services/viewlet/browser/viewletService.ts @@ -33,6 +33,7 @@ export class ViewletService implements IViewletService { private _onDidViewletEnable = new Emitter<{ id: string, enabled: boolean }>(); private disposables: IDisposable[] = []; + public get onDidViewletRegister(): Event { return >this.viewletRegistry.onDidRegister; } public get onDidViewletOpen(): Event { return this.sidebarPart.onDidViewletOpen; } public get onDidViewletClose(): Event { return this.sidebarPart.onDidViewletClose; } public get onDidViewletEnablementChange(): Event<{ id: string, enabled: boolean }> { return this._onDidViewletEnable.event; } diff --git a/src/vs/workbench/services/workspace/node/workspaceEditingService.ts b/src/vs/workbench/services/workspace/node/workspaceEditingService.ts index f5acaa9c5ba..5e8f0fb6ca9 100644 --- a/src/vs/workbench/services/workspace/node/workspaceEditingService.ts +++ b/src/vs/workbench/services/workspace/node/workspaceEditingService.ts @@ -155,31 +155,33 @@ export class WorkspaceEditingService implements IWorkspaceEditingService { private handleWorkspaceConfigurationEditingError(error: JSONEditingError): TPromise { switch (error.code) { case JSONEditingErrorCode.ERROR_INVALID_FILE: - return this.onInvalidWorkspaceConfigurationFileError(); + this.onInvalidWorkspaceConfigurationFileError(); + return TPromise.as(void 0); case JSONEditingErrorCode.ERROR_FILE_DIRTY: - return this.onWorkspaceConfigurationFileDirtyError(); + this.onWorkspaceConfigurationFileDirtyError(); + return TPromise.as(void 0); } this.notificationService.error(error.message); return TPromise.as(void 0); } - private onInvalidWorkspaceConfigurationFileError(): TPromise { + private onInvalidWorkspaceConfigurationFileError(): void { const message = nls.localize('errorInvalidTaskConfiguration', "Unable to write into workspace configuration file. Please open the file to correct errors/warnings in it and try again."); - return this.askToOpenWorkspaceConfigurationFile(message); + this.askToOpenWorkspaceConfigurationFile(message); } - private onWorkspaceConfigurationFileDirtyError(): TPromise { + private onWorkspaceConfigurationFileDirtyError(): void { const message = nls.localize('errorWorkspaceConfigurationFileDirty', "Unable to write into workspace configuration file because the file is dirty. Please save it and try again."); - return this.askToOpenWorkspaceConfigurationFile(message); + this.askToOpenWorkspaceConfigurationFile(message); } - private askToOpenWorkspaceConfigurationFile(message: string): TPromise { - return this.notificationService.prompt(Severity.Error, message, [nls.localize('openWorkspaceConfigurationFile', "Open Workspace Configuration")]) - .then(option => { - if (option === 0) { - this.commandService.executeCommand('workbench.action.openWorkspaceConfigFile'); - } - }); + private askToOpenWorkspaceConfigurationFile(message: string): void { + this.notificationService.prompt(Severity.Error, message, + [{ + label: nls.localize('openWorkspaceConfigurationFile', "Open Workspace Configuration"), + run: () => this.commandService.executeCommand('workbench.action.openWorkspaceConfigFile') + }] + ); } private doEnterWorkspace(mainSidePromise: () => TPromise): TPromise { diff --git a/src/vs/workbench/test/browser/part.test.ts b/src/vs/workbench/test/browser/part.test.ts index 6eaae451336..4ba14edc657 100644 --- a/src/vs/workbench/test/browser/part.test.ts +++ b/src/vs/workbench/test/browser/part.test.ts @@ -6,7 +6,7 @@ 'use strict'; import * as assert from 'assert'; -import { Build, Builder } from 'vs/base/browser/builder'; +import { Builder, $ } from 'vs/base/browser/builder'; import { Part } from 'vs/workbench/browser/part'; import * as Types from 'vs/base/common/types'; import { IStorageService } from 'vs/platform/storage/common/storage'; @@ -16,16 +16,16 @@ import { TestWorkspace } from 'vs/platform/workspace/test/common/testWorkspace'; class MyPart extends Part { - constructor(private expectedParent: Builder) { + constructor(private expectedParent: HTMLElement) { super('myPart', { hasTitle: true }, new TestThemeService()); } - public createTitleArea(parent: Builder): Builder { + public createTitleArea(parent: HTMLElement): HTMLElement { assert.strictEqual(parent, this.expectedParent); return super.createTitleArea(parent); } - public createContentArea(parent: Builder): Builder { + public createContentArea(parent: HTMLElement): HTMLElement { assert.strictEqual(parent, this.expectedParent); return super.createContentArea(parent); } @@ -41,22 +41,22 @@ class MyPart2 extends Part { super('myPart2', { hasTitle: true }, new TestThemeService()); } - public createTitleArea(parent: Builder): Builder { - return parent.div(function (div) { + public createTitleArea(parent: HTMLElement): HTMLElement { + return $(parent).div(function (div) { div.span({ id: 'myPart.title', innerHtml: 'Title' }); - }); + }).getHTMLElement(); } - public createContentArea(parent: Builder): Builder { - return parent.div(function (div) { + public createContentArea(parent: HTMLElement): HTMLElement { + return $(parent).div(function (div) { div.span({ id: 'myPart.content', innerHtml: 'Content' }); - }); + }).getHTMLElement(); } } @@ -66,17 +66,17 @@ class MyPart3 extends Part { super('myPart2', { hasTitle: false }, new TestThemeService()); } - public createTitleArea(parent: Builder): Builder { + public createTitleArea(parent: HTMLElement): HTMLElement { return null; } - public createContentArea(parent: Builder): Builder { - return parent.div(function (div) { + public createContentArea(parent: HTMLElement): HTMLElement { + return $(parent).div(function (div) { div.span({ id: 'myPart.content', innerHtml: 'Content' }); - }); + }).getHTMLElement(); } } @@ -97,14 +97,13 @@ suite('Workbench Part', () => { }); test('Creation', function () { - let b = Build.withElementById(fixtureId); + let b = new Builder(document.getElementById(fixtureId)); b.div().hide(); - let part = new MyPart(b); - part.create(b); + let part = new MyPart(b.getHTMLElement()); + part.create(b.getHTMLElement()); assert.strictEqual(part.getId(), 'myPart'); - assert.strictEqual(part.getContainer(), b); // Memento let memento = part.getMemento(storage); @@ -115,7 +114,7 @@ suite('Workbench Part', () => { part.shutdown(); // Re-Create to assert memento contents - part = new MyPart(b); + part = new MyPart(b.getHTMLElement()); memento = part.getMemento(storage); assert(memento); @@ -127,31 +126,31 @@ suite('Workbench Part', () => { delete memento.bar; part.shutdown(); - part = new MyPart(b); + part = new MyPart(b.getHTMLElement()); memento = part.getMemento(storage); assert(memento); assert.strictEqual(Types.isEmptyObject(memento), true); }); test('Part Layout with Title and Content', function () { - let b = Build.withElementById(fixtureId); + let b = new Builder(document.getElementById(fixtureId)); b.div().hide(); let part = new MyPart2(); - part.create(b); + part.create(b.getHTMLElement()); - assert(Build.withElementById('myPart.title')); - assert(Build.withElementById('myPart.content')); + assert(document.getElementById('myPart.title')); + assert(document.getElementById('myPart.content')); }); test('Part Layout with Content only', function () { - let b = Build.withElementById(fixtureId); + let b = new Builder(document.getElementById(fixtureId)); b.div().hide(); let part = new MyPart3(); - part.create(b); + part.create(b.getHTMLElement()); - assert(!Build.withElementById('myPart.title')); - assert(Build.withElementById('myPart.content')); + assert(!document.getElementById('myPart.title')); + assert(document.getElementById('myPart.content')); }); }); \ No newline at end of file diff --git a/src/vs/workbench/test/browser/parts/views/contributableViews.test.ts b/src/vs/workbench/test/browser/parts/views/contributableViews.test.ts new file mode 100644 index 00000000000..4153fce6fc1 --- /dev/null +++ b/src/vs/workbench/test/browser/parts/views/contributableViews.test.ts @@ -0,0 +1,242 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { ContributableViewsModel } from 'vs/workbench/browser/parts/views/contributableViews'; +import { ViewLocation, ViewsRegistry, IViewDescriptor } from 'vs/workbench/common/views'; +import { ContextKeyService } from 'vs/platform/contextkey/browser/contextKeyService'; +import { IContextKeyService, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { SimpleConfigurationService } from 'vs/editor/standalone/browser/simpleServices'; +import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { move } from 'vs/base/common/arrays'; + +const location = ViewLocation.register('test'); + +class ViewDescriptorSequence { + + readonly elements: IViewDescriptor[]; + private disposables: IDisposable[] = []; + + constructor(model: ContributableViewsModel) { + this.elements = [...model.visibleViewDescriptors]; + model.onDidAdd(({ viewDescriptor, index }) => this.elements.splice(index, 0, viewDescriptor), null, this.disposables); + model.onDidRemove(({ viewDescriptor, index }) => this.elements.splice(index, 1), null, this.disposables); + model.onDidMove(({ from, to }) => move(this.elements, from.index, to.index), null, this.disposables); + } + + dispose() { + this.disposables = dispose(this.disposables); + } +} + +suite('ContributableViewsModel', () => { + let contextKeyService: IContextKeyService; + + setup(() => { + const configurationService = new SimpleConfigurationService(); + contextKeyService = new ContextKeyService(configurationService); + }); + + teardown(() => { + contextKeyService.dispose(); + }); + + test('empty model', function () { + const model = new ContributableViewsModel(location, contextKeyService); + assert.equal(model.visibleViewDescriptors.length, 0); + }); + + test('register/unregister', function () { + const model = new ContributableViewsModel(location, contextKeyService); + const seq = new ViewDescriptorSequence(model); + + assert.equal(model.visibleViewDescriptors.length, 0); + assert.equal(seq.elements.length, 0); + + const viewDescriptor: IViewDescriptor = { + id: 'view1', + ctor: null, + location, + name: 'Test View 1' + }; + + ViewsRegistry.registerViews([viewDescriptor]); + + assert.equal(model.visibleViewDescriptors.length, 1); + assert.equal(seq.elements.length, 1); + assert.deepEqual(model.visibleViewDescriptors[0], viewDescriptor); + assert.deepEqual(seq.elements[0], viewDescriptor); + + ViewsRegistry.deregisterViews(['view1'], location); + + assert.equal(model.visibleViewDescriptors.length, 0); + assert.equal(seq.elements.length, 0); + }); + + test('when contexts', async function () { + const model = new ContributableViewsModel(location, contextKeyService); + const seq = new ViewDescriptorSequence(model); + + assert.equal(model.visibleViewDescriptors.length, 0); + assert.equal(seq.elements.length, 0); + + const viewDescriptor: IViewDescriptor = { + id: 'view1', + ctor: null, + location, + name: 'Test View 1', + when: ContextKeyExpr.equals('showview1', true) + }; + + ViewsRegistry.registerViews([viewDescriptor]); + assert.equal(model.visibleViewDescriptors.length, 0, 'view should not appear since context isnt in'); + assert.equal(seq.elements.length, 0); + + const key = contextKeyService.createKey('showview1', false); + assert.equal(model.visibleViewDescriptors.length, 0, 'view should still not appear since showview1 isnt true'); + assert.equal(seq.elements.length, 0); + + key.set(true); + await new Promise(c => setTimeout(c, 30)); + assert.equal(model.visibleViewDescriptors.length, 1, 'view should appear'); + assert.equal(seq.elements.length, 1); + assert.deepEqual(model.visibleViewDescriptors[0], viewDescriptor); + assert.equal(seq.elements[0], viewDescriptor); + + key.set(false); + await new Promise(c => setTimeout(c, 30)); + assert.equal(model.visibleViewDescriptors.length, 0, 'view should disappear'); + assert.equal(seq.elements.length, 0); + + ViewsRegistry.deregisterViews(['view1'], location); + assert.equal(model.visibleViewDescriptors.length, 0, 'view should not be there anymore'); + assert.equal(seq.elements.length, 0); + + key.set(true); + await new Promise(c => setTimeout(c, 30)); + assert.equal(model.visibleViewDescriptors.length, 0, 'view should not be there anymore'); + assert.equal(seq.elements.length, 0); + }); + + test('when contexts - multiple', async function () { + const model = new ContributableViewsModel(location, contextKeyService); + const seq = new ViewDescriptorSequence(model); + + const view1: IViewDescriptor = { id: 'view1', ctor: null, location, name: 'Test View 1' }; + const view2: IViewDescriptor = { id: 'view2', ctor: null, location, name: 'Test View 2', when: ContextKeyExpr.equals('showview2', true) }; + + ViewsRegistry.registerViews([view1, view2]); + assert.deepEqual(model.visibleViewDescriptors, [view1], 'only view1 should be visible'); + assert.deepEqual(seq.elements, [view1], 'only view1 should be visible'); + + const key = contextKeyService.createKey('showview2', false); + assert.deepEqual(model.visibleViewDescriptors, [view1], 'still only view1 should be visible'); + assert.deepEqual(seq.elements, [view1], 'still only view1 should be visible'); + + key.set(true); + await new Promise(c => setTimeout(c, 30)); + assert.deepEqual(model.visibleViewDescriptors, [view1, view2], 'both views should be visible'); + assert.deepEqual(seq.elements, [view1, view2], 'both views should be visible'); + + ViewsRegistry.deregisterViews([view1.id, view2.id], location); + }); + + test('when contexts - multiple 2', async function () { + const model = new ContributableViewsModel(location, contextKeyService); + const seq = new ViewDescriptorSequence(model); + + const view1: IViewDescriptor = { id: 'view1', ctor: null, location, name: 'Test View 1', when: ContextKeyExpr.equals('showview1', true) }; + const view2: IViewDescriptor = { id: 'view2', ctor: null, location, name: 'Test View 2' }; + + ViewsRegistry.registerViews([view1, view2]); + assert.deepEqual(model.visibleViewDescriptors, [view2], 'only view2 should be visible'); + assert.deepEqual(seq.elements, [view2], 'only view2 should be visible'); + + const key = contextKeyService.createKey('showview1', false); + assert.deepEqual(model.visibleViewDescriptors, [view2], 'still only view2 should be visible'); + assert.deepEqual(seq.elements, [view2], 'still only view2 should be visible'); + + key.set(true); + await new Promise(c => setTimeout(c, 30)); + assert.deepEqual(model.visibleViewDescriptors, [view1, view2], 'both views should be visible'); + assert.deepEqual(seq.elements, [view1, view2], 'both views should be visible'); + + ViewsRegistry.deregisterViews([view1.id, view2.id], location); + }); + + test('setVisible', function () { + const model = new ContributableViewsModel(location, contextKeyService); + const seq = new ViewDescriptorSequence(model); + + const view1: IViewDescriptor = { id: 'view1', ctor: null, location, name: 'Test View 1', canToggleVisibility: true }; + const view2: IViewDescriptor = { id: 'view2', ctor: null, location, name: 'Test View 2', canToggleVisibility: true }; + const view3: IViewDescriptor = { id: 'view3', ctor: null, location, name: 'Test View 3', canToggleVisibility: true }; + + ViewsRegistry.registerViews([view1, view2, view3]); + assert.deepEqual(model.visibleViewDescriptors, [view1, view2, view3]); + assert.deepEqual(seq.elements, [view1, view2, view3]); + + model.setVisible('view2', true); + assert.deepEqual(model.visibleViewDescriptors, [view1, view2, view3], 'nothing should happen'); + assert.deepEqual(seq.elements, [view1, view2, view3]); + + model.setVisible('view2', false); + assert.deepEqual(model.visibleViewDescriptors, [view1, view3], 'view2 should hide'); + assert.deepEqual(seq.elements, [view1, view3]); + + model.setVisible('view1', false); + assert.deepEqual(model.visibleViewDescriptors, [view3], 'view1 should hide'); + assert.deepEqual(seq.elements, [view3]); + + model.setVisible('view3', false); + assert.deepEqual(model.visibleViewDescriptors, [], 'view3 shoud hide'); + assert.deepEqual(seq.elements, []); + + model.setVisible('view1', true); + assert.deepEqual(model.visibleViewDescriptors, [view1], 'view1 should show'); + assert.deepEqual(seq.elements, [view1]); + + model.setVisible('view3', true); + assert.deepEqual(model.visibleViewDescriptors, [view1, view3], 'view3 should show'); + assert.deepEqual(seq.elements, [view1, view3]); + + model.setVisible('view2', true); + assert.deepEqual(model.visibleViewDescriptors, [view1, view2, view3], 'view2 should show'); + assert.deepEqual(seq.elements, [view1, view2, view3]); + + ViewsRegistry.deregisterViews([view1.id, view2.id, view3.id], location); + assert.deepEqual(model.visibleViewDescriptors, []); + assert.deepEqual(seq.elements, []); + }); + + test('move', function () { + const model = new ContributableViewsModel(location, contextKeyService); + const seq = new ViewDescriptorSequence(model); + + const view1: IViewDescriptor = { id: 'view1', ctor: null, location, name: 'Test View 1' }; + const view2: IViewDescriptor = { id: 'view2', ctor: null, location, name: 'Test View 2' }; + const view3: IViewDescriptor = { id: 'view3', ctor: null, location, name: 'Test View 3' }; + + ViewsRegistry.registerViews([view1, view2, view3]); + assert.deepEqual(model.visibleViewDescriptors, [view1, view2, view3], 'model views should be OK'); + assert.deepEqual(seq.elements, [view1, view2, view3], 'sql views should be OK'); + + model.move('view3', 'view1'); + assert.deepEqual(model.visibleViewDescriptors, [view3, view1, view2], 'view3 should go to the front'); + assert.deepEqual(seq.elements, [view3, view1, view2]); + + model.move('view1', 'view2'); + assert.deepEqual(model.visibleViewDescriptors, [view3, view2, view1], 'view1 should go to the end'); + assert.deepEqual(seq.elements, [view3, view2, view1]); + + model.move('view1', 'view3'); + assert.deepEqual(model.visibleViewDescriptors, [view1, view3, view2], 'view1 should go to the front'); + assert.deepEqual(seq.elements, [view1, view3, view2]); + + model.move('view2', 'view3'); + assert.deepEqual(model.visibleViewDescriptors, [view1, view2, view3], 'view2 should go to the middle'); + assert.deepEqual(seq.elements, [view1, view2, view3]); + }); +}); diff --git a/src/vs/workbench/test/common/editor/editor.test.ts b/src/vs/workbench/test/common/editor/editor.test.ts index 218ff420427..fcfba8f9799 100644 --- a/src/vs/workbench/test/common/editor/editor.test.ts +++ b/src/vs/workbench/test/common/editor/editor.test.ts @@ -7,9 +7,9 @@ import * as assert from 'assert'; import { TPromise } from 'vs/base/common/winjs.base'; -import { EditorInput, toResource } from 'vs/workbench/common/editor'; +import { EditorInput, toResource, EditorViewStateMemento } from 'vs/workbench/common/editor'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; -import { IEditorModel } from 'vs/platform/editor/common/editor'; +import { IEditorModel, Position } from 'vs/platform/editor/common/editor'; import URI from 'vs/base/common/uri'; import { IUntitledEditorService, UntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -86,4 +86,129 @@ suite('Workbench - Editor', () => { assert.equal(toResource(file, { supportSideBySide: true, filter: Schemas.file }).toString(), file.getResource().toString()); assert.equal(toResource(file, { supportSideBySide: true, filter: [Schemas.file, Schemas.untitled] }).toString(), file.getResource().toString()); }); + + test('EditorViewStateMemento - basics', function () { + interface TestViewState { + line: number; + } + + const rawMemento = Object.create(null); + let memento = new EditorViewStateMemento(rawMemento, 'key', 3); + + let res = memento.loadState(URI.file('/A'), Position.ONE); + assert.ok(!res); + + memento.saveState(URI.file('/A'), Position.ONE, { line: 3 }); + res = memento.loadState(URI.file('/A'), Position.ONE); + assert.ok(res); + assert.equal(res.line, 3); + + memento.saveState(URI.file('/A'), Position.TWO, { line: 5 }); + res = memento.loadState(URI.file('/A'), Position.TWO); + assert.ok(res); + assert.equal(res.line, 5); + + // Ensure capped at 3 elements + memento.saveState(URI.file('/B'), Position.ONE, { line: 1 }); + memento.saveState(URI.file('/C'), Position.ONE, { line: 1 }); + memento.saveState(URI.file('/D'), Position.ONE, { line: 1 }); + memento.saveState(URI.file('/E'), Position.ONE, { line: 1 }); + + assert.ok(!memento.loadState(URI.file('/A'), Position.ONE)); + assert.ok(!memento.loadState(URI.file('/B'), Position.ONE)); + assert.ok(memento.loadState(URI.file('/C'), Position.ONE)); + assert.ok(memento.loadState(URI.file('/D'), Position.ONE)); + assert.ok(memento.loadState(URI.file('/E'), Position.ONE)); + + memento.save(); + + memento = new EditorViewStateMemento(rawMemento, 'key', 3); + assert.ok(memento.loadState(URI.file('/C'), Position.ONE)); + assert.ok(memento.loadState(URI.file('/D'), Position.ONE)); + assert.ok(memento.loadState(URI.file('/E'), Position.ONE)); + + memento.clearState(URI.file('/C')); + memento.clearState(URI.file('/E')); + + assert.ok(!memento.loadState(URI.file('/C'), Position.ONE)); + assert.ok(memento.loadState(URI.file('/D'), Position.ONE)); + assert.ok(!memento.loadState(URI.file('/E'), Position.ONE)); + }); + + test('EditorViewStateMemento - use with editor input', function () { + interface TestViewState { + line: number; + } + + class TestEditorInput extends EditorInput { + constructor(private resource: URI, private id = 'testEditorInput') { + super(); + } + public getTypeId() { return 'testEditorInput'; } + public resolve(): TPromise { return null; } + + public matches(other: TestEditorInput): boolean { + return other && this.id === other.id && other instanceof TestEditorInput; + } + + public getResource(): URI { + return this.resource; + } + } + + const rawMemento = Object.create(null); + let memento = new EditorViewStateMemento(rawMemento, 'key', 3); + + const testInputA = new TestEditorInput(URI.file('/A')); + + let res = memento.loadState(testInputA, Position.ONE); + assert.ok(!res); + + memento.saveState(testInputA, Position.ONE, { line: 3 }); + res = memento.loadState(testInputA, Position.ONE); + assert.ok(res); + assert.equal(res.line, 3); + + // State removed when input gets disposed + testInputA.dispose(); + res = memento.loadState(testInputA, Position.ONE); + assert.ok(!res); + }); + + test('EditorViewStateMemento - migration', function () { + interface TestViewState { + line: number; + } + + const rawMemento = { + 'key': { + [URI.file('/A').toString()]: { + 0: { + line: 5 + } + }, + [URI.file('/B').toString()]: { + 0: { + line: 1 + }, + 1: { + line: 2 + } + } + } + }; + let memento = new EditorViewStateMemento(rawMemento, 'key', 3); + + let res = memento.loadState(URI.file('/A'), Position.ONE); + assert.ok(res); + assert.equal(res.line, 5); + + res = memento.loadState(URI.file('/B'), Position.ONE); + assert.ok(res); + assert.equal(res.line, 1); + + res = memento.loadState(URI.file('/B'), Position.TWO); + assert.ok(res); + assert.equal(res.line, 2); + }); }); \ No newline at end of file diff --git a/src/vs/workbench/test/common/notifications.test.ts b/src/vs/workbench/test/common/notifications.test.ts index 533cc9ae266..fc7699a81f1 100644 --- a/src/vs/workbench/test/common/notifications.test.ts +++ b/src/vs/workbench/test/common/notifications.test.ts @@ -96,11 +96,11 @@ suite('Notifications', () => { assert.equal(called, 1); called = 0; - item1.onDidDispose(() => { + item1.onDidClose(() => { called++; }); - item1.dispose(); + item1.close(); assert.equal(called, 1); // Error with Action @@ -157,11 +157,11 @@ suite('Notifications', () => { assert.equal(model.notifications.length, 3); let called = 0; - item1Handle.onDidDispose(() => { + item1Handle.onDidClose(() => { called++; }); - item1Handle.dispose(); + item1Handle.close(); assert.equal(called, 1); assert.equal(model.notifications.length, 2); assert.equal(lastEvent.item.severity, item1.severity); @@ -176,7 +176,7 @@ suite('Notifications', () => { assert.equal(lastEvent.index, 0); assert.equal(lastEvent.kind, NotificationChangeType.ADD); - item2Handle.dispose(); + item2Handle.close(); assert.equal(model.notifications.length, 1); assert.equal(lastEvent.item.severity, item2Duplicate.severity); assert.equal(lastEvent.item.message.value, item2Duplicate.message); diff --git a/src/vs/workbench/test/electron-browser/api/extHostApiCommands.test.ts b/src/vs/workbench/test/electron-browser/api/extHostApiCommands.test.ts index 5e6b7013ec1..4801ffcda53 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostApiCommands.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostApiCommands.test.ts @@ -129,7 +129,7 @@ suite('ExtHostLanguageFeatureCommands', function () { const diagnostics = new ExtHostDiagnostics(rpcProtocol); rpcProtocol.set(ExtHostContext.ExtHostDiagnostics, diagnostics); - extHost = new ExtHostLanguageFeatures(rpcProtocol, extHostDocuments, commands, heapService, diagnostics); + extHost = new ExtHostLanguageFeatures(rpcProtocol, null, extHostDocuments, commands, heapService, diagnostics); rpcProtocol.set(ExtHostContext.ExtHostLanguageFeatures, extHost); mainThread = rpcProtocol.set(MainContext.MainThreadLanguageFeatures, inst.createInstance(MainThreadLanguageFeatures, rpcProtocol)); @@ -422,6 +422,39 @@ suite('ExtHostLanguageFeatureCommands', function () { }); }); + test('Suggest, resolve completion items', async function () { + + let resolveCount = 0; + + disposables.push(extHost.registerCompletionItemProvider(defaultSelector, { + provideCompletionItems(): any { + let a = new types.CompletionItem('item1'); + let b = new types.CompletionItem('item2'); + let c = new types.CompletionItem('item3'); + let d = new types.CompletionItem('item4'); + return new types.CompletionList([a, b, c, d], false); + }, + resolveCompletionItem(item) { + resolveCount += 1; + return item; + } + }, [])); + + await rpcProtocol.sync(); + + let list = await commands.executeCommand( + 'vscode.executeCompletionItemProvider', + model.uri, + new types.Position(0, 4), + undefined, + 2 // maxItemsToResolve + ); + + assert.ok(list instanceof types.CompletionList); + assert.equal(resolveCount, 2); + + }); + // --- quickfix test('QuickFix, back and forth', function () { @@ -501,6 +534,40 @@ suite('ExtHostLanguageFeatureCommands', function () { }); }); + test('CodeLens, resolve', async function () { + + let resolveCount = 0; + + disposables.push(extHost.registerCodeLensProvider(defaultSelector, { + provideCodeLenses(): any { + return [ + new types.CodeLens(new types.Range(0, 0, 1, 1)), + new types.CodeLens(new types.Range(0, 0, 1, 1)), + new types.CodeLens(new types.Range(0, 0, 1, 1)), + new types.CodeLens(new types.Range(0, 0, 1, 1), { title: 'Already resolved', command: 'fff' }) + ]; + }, + resolveCodeLens(codeLens: types.CodeLens) { + codeLens.command = { title: resolveCount.toString(), command: 'resolved' }; + resolveCount += 1; + return codeLens; + } + })); + + await rpcProtocol.sync(); + + let value = await commands.executeCommand('vscode.executeCodeLensProvider', model.uri, 2); + + assert.equal(value.length, 3); // the resolve argument defines the number of results being returned + assert.equal(resolveCount, 2); + + resolveCount = 0; + value = await commands.executeCommand('vscode.executeCodeLensProvider', model.uri); + + assert.equal(value.length, 4); + assert.equal(resolveCount, 0); + }); + test('Links, back and forth', function () { disposables.push(extHost.registerDocumentLinkProvider(defaultSelector, { @@ -522,4 +589,55 @@ suite('ExtHostLanguageFeatureCommands', function () { }); }); }); + + + test('Color provider', function () { + + disposables.push(extHost.registerColorProvider(defaultSelector, { + provideDocumentColors(): vscode.ColorInformation[] { + return [new types.ColorInformation(new types.Range(0, 0, 0, 20), new types.Color(0.1, 0.2, 0.3, 0.4))]; + }, + provideColorPresentations(color: vscode.Color, context: { range: vscode.Range, document: vscode.TextDocument }): vscode.ColorPresentation[] { + const cp = new types.ColorPresentation('#ABC'); + cp.textEdit = types.TextEdit.replace(new types.Range(1, 0, 1, 20), '#ABC'); + cp.additionalTextEdits = [types.TextEdit.insert(new types.Position(2, 20), '*')]; + return [cp]; + } + })); + + return rpcProtocol.sync().then(() => { + return commands.executeCommand('vscode.executeDocumentColorProvider', model.uri).then(value => { + assert.equal(value.length, 1); + let [first] = value; + + assert.equal(first.color.red, 0.1); + assert.equal(first.color.green, 0.2); + assert.equal(first.color.blue, 0.3); + assert.equal(first.color.alpha, 0.4); + assert.equal(first.range.start.line, 0); + assert.equal(first.range.start.character, 0); + assert.equal(first.range.end.line, 0); + assert.equal(first.range.end.character, 20); + }); + }).then(() => { + const color = new types.Color(0.5, 0.6, 0.7, 0.8); + const range = new types.Range(0, 0, 0, 20); + return commands.executeCommand('vscode.executeColorPresentationProvider', color, { uri: model.uri, range }).then(value => { + assert.equal(value.length, 1); + let [first] = value; + + assert.equal(first.label, '#ABC'); + assert.equal(first.textEdit.newText, '#ABC'); + assert.equal(first.textEdit.range.start.line, 1); + assert.equal(first.textEdit.range.start.character, 0); + assert.equal(first.textEdit.range.end.line, 1); + assert.equal(first.textEdit.range.end.character, 20); + assert.equal(first.additionalTextEdits.length, 1); + assert.equal(first.additionalTextEdits[0].range.start.line, 2); + assert.equal(first.additionalTextEdits[0].range.start.character, 20); + assert.equal(first.additionalTextEdits[0].range.end.line, 2); + assert.equal(first.additionalTextEdits[0].range.end.character, 20); + }); + }); + }); }); diff --git a/src/vs/workbench/test/electron-browser/api/extHostConfiguration.test.ts b/src/vs/workbench/test/electron-browser/api/extHostConfiguration.test.ts index 382cbc08b75..6638722bd03 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostConfiguration.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostConfiguration.test.ts @@ -17,6 +17,7 @@ import { mock } from 'vs/workbench/test/electron-browser/api/mock'; import { IWorkspaceFolder, WorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; import { NullLogService } from 'vs/platform/log/common/log'; +import { assign } from 'vs/base/common/objects'; suite('ExtHostConfiguration', function () { @@ -41,7 +42,8 @@ suite('ExtHostConfiguration', function () { user: new ConfigurationModel(contents), workspace: new ConfigurationModel(), folders: Object.create(null), - configurationScopes: {} + configurationScopes: {}, + isComplete: true }; } @@ -146,6 +148,88 @@ suite('ExtHostConfiguration', function () { assert.equal(actual['statusBar.foreground'], 'somevalue'); }); + test('Stringify returned configuration', function () { + + const all = createExtHostConfiguration({ + 'farboo': { + 'config0': true, + 'nested': { + 'config1': 42, + 'config2': 'Das Pferd frisst kein Reis.' + }, + 'config4': '' + }, + 'workbench': { + 'colorCustomizations': { + 'statusBar.foreground': 'somevalue' + }, + 'emptyobjectkey': { + } + } + }); + + let testObject = all.getConfiguration(); + let actual = testObject.get('farboo'); + assert.deepEqual(JSON.stringify({ + 'config0': true, + 'nested': { + 'config1': 42, + 'config2': 'Das Pferd frisst kein Reis.' + }, + 'config4': '' + }), JSON.stringify(actual)); + + assert.deepEqual(undefined, JSON.stringify(testObject.get('unknownkey'))); + + actual = testObject.get('farboo'); + actual['config0'] = false; + assert.deepEqual(JSON.stringify({ + 'config0': false, + 'nested': { + 'config1': 42, + 'config2': 'Das Pferd frisst kein Reis.' + }, + 'config4': '' + }), JSON.stringify(actual)); + + actual = testObject.get('workbench')['colorCustomizations']; + actual['statusBar.background'] = 'anothervalue'; + assert.deepEqual(JSON.stringify({ + 'statusBar.foreground': 'somevalue', + 'statusBar.background': 'anothervalue' + }), JSON.stringify(actual)); + + actual = testObject.get('workbench'); + actual['unknownkey'] = 'somevalue'; + assert.deepEqual(JSON.stringify({ + 'colorCustomizations': { + 'statusBar.foreground': 'somevalue' + }, + 'emptyobjectkey': {}, + 'unknownkey': 'somevalue' + }), JSON.stringify(actual)); + + actual = all.getConfiguration('workbench').get('emptyobjectkey'); + actual = assign(actual || {}, { + 'statusBar.background': `#0ff`, + 'statusBar.foreground': `#ff0`, + }); + assert.deepEqual(JSON.stringify({ + 'statusBar.background': `#0ff`, + 'statusBar.foreground': `#ff0`, + }), JSON.stringify(actual)); + + actual = all.getConfiguration('workbench').get('unknownkey'); + actual = assign(actual || {}, { + 'statusBar.background': `#0ff`, + 'statusBar.foreground': `#ff0`, + }); + assert.deepEqual(JSON.stringify({ + 'statusBar.background': `#0ff`, + 'statusBar.foreground': `#ff0`, + }), JSON.stringify(actual)); + }); + test('cannot modify returned configuration', function () { const all = createExtHostConfiguration({ @@ -197,7 +281,8 @@ suite('ExtHostConfiguration', function () { }, ['editor.wordWrap']), workspace: new ConfigurationModel({}, []), folders: Object.create(null), - configurationScopes: {} + configurationScopes: {}, + isComplete: true } ); @@ -243,7 +328,8 @@ suite('ExtHostConfiguration', function () { }, ['editor.wordWrap']), workspace, folders, - configurationScopes: {} + configurationScopes: {}, + isComplete: true } ); @@ -317,7 +403,8 @@ suite('ExtHostConfiguration', function () { }, ['editor.wordWrap']), workspace, folders, - configurationScopes: {} + configurationScopes: {}, + isComplete: true } ); 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 f808f44c17f..daad9c035e9 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostDocumentData.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostDocumentData.test.ts @@ -101,6 +101,7 @@ suite('ExtHostDocumentData', () => { data.onEvents({ changes: [{ range: { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 }, + rangeOffset: undefined, rangeLength: undefined, text: '\t ' }], @@ -157,6 +158,7 @@ suite('ExtHostDocumentData', () => { data.onEvents({ changes: [{ range: { startLineNumber: 1, startColumn: 3, endLineNumber: 1, endColumn: 6 }, + rangeOffset: undefined, rangeLength: undefined, text: '' }], @@ -174,6 +176,7 @@ suite('ExtHostDocumentData', () => { data.onEvents({ changes: [{ range: { startLineNumber: 1, startColumn: 3, endLineNumber: 1, endColumn: 6 }, + rangeOffset: undefined, rangeLength: undefined, text: 'is could be' }], @@ -191,6 +194,7 @@ suite('ExtHostDocumentData', () => { data.onEvents({ changes: [{ range: { startLineNumber: 1, startColumn: 3, endLineNumber: 1, endColumn: 6 }, + rangeOffset: undefined, rangeLength: undefined, text: 'is could be\na line with number' }], @@ -211,6 +215,7 @@ suite('ExtHostDocumentData', () => { data.onEvents({ changes: [{ range: { startLineNumber: 1, startColumn: 3, endLineNumber: 2, endColumn: 6 }, + rangeOffset: undefined, rangeLength: undefined, text: '' }], @@ -344,6 +349,7 @@ suite('ExtHostDocumentData updates line mapping', () => { return { changes: [{ range: range, + rangeOffset: undefined, rangeLength: undefined, text: text }], diff --git a/src/vs/workbench/test/electron-browser/api/extHostDocumentSaveParticipant.test.ts b/src/vs/workbench/test/electron-browser/api/extHostDocumentSaveParticipant.test.ts index b44d25fba63..a8c6bfbfcdf 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostDocumentSaveParticipant.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostDocumentSaveParticipant.test.ts @@ -302,6 +302,7 @@ suite('ExtHostDocumentSaveParticipant', () => { documents.$acceptModelChanged(resource, { changes: [{ range: { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 }, + rangeOffset: undefined, rangeLength: undefined, text: 'bar' }], @@ -337,6 +338,7 @@ suite('ExtHostDocumentSaveParticipant', () => { changes: [{ range, text, + rangeOffset: undefined, rangeLength: undefined, }], eol: undefined, 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 ad73ec6a899..f0748586c5d 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostLanguageFeatures.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostLanguageFeatures.test.ts @@ -30,7 +30,7 @@ import { getDefinitionsAtPosition, getImplementationsAtPosition, getTypeDefiniti import { getHover } from 'vs/editor/contrib/hover/getHover'; import { getOccurrencesAtPosition } from 'vs/editor/contrib/wordHighlighter/wordHighlighter'; import { provideReferences } from 'vs/editor/contrib/referenceSearch/referenceSearch'; -import { getCodeActions } from 'vs/editor/contrib/quickFix/quickFix'; +import { getCodeActions } from 'vs/editor/contrib/codeAction/codeAction'; import { getWorkspaceSymbols } from 'vs/workbench/parts/search/common/search'; import { rename } from 'vs/editor/contrib/rename/rename'; import { provideSignatureHelp } from 'vs/editor/contrib/parameterHints/provideSignatureHelp'; @@ -45,6 +45,7 @@ import * as vscode from 'vscode'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { NullLogService } from 'vs/platform/log/common/log'; import { ITextModel, EndOfLineSequence } from 'vs/editor/common/model'; +import { getColors } from 'vs/editor/contrib/colorPicker/color'; const defaultSelector = { scheme: 'far' }; const model: ITextModel = EditorModel.createFromString( @@ -110,7 +111,7 @@ suite('ExtHostLanguageFeatures', function () { const diagnostics = new ExtHostDiagnostics(rpcProtocol); rpcProtocol.set(ExtHostContext.ExtHostDiagnostics, diagnostics); - extHost = new ExtHostLanguageFeatures(rpcProtocol, extHostDocuments, commands, heapService, diagnostics); + extHost = new ExtHostLanguageFeatures(rpcProtocol, null, extHostDocuments, commands, heapService, diagnostics); rpcProtocol.set(ExtHostContext.ExtHostLanguageFeatures, extHost); mainThread = rpcProtocol.set(MainContext.MainThreadLanguageFeatures, inst.createInstance(MainThreadLanguageFeatures, rpcProtocol)); @@ -1033,6 +1034,13 @@ suite('ExtHostLanguageFeatures', function () { }); test('Format Doc, order', function () { + + disposables.push(extHost.registerDocumentFormattingEditProvider(defaultSelector, { + provideDocumentFormattingEdits(): any { + return undefined; + } + })); + disposables.push(extHost.registerDocumentFormattingEditProvider(defaultSelector, { provideDocumentFormattingEdits(): any { return [new types.TextEdit(new types.Range(0, 0, 0, 0), 'testing')]; @@ -1078,6 +1086,11 @@ suite('ExtHostLanguageFeatures', function () { return [new types.TextEdit(new types.Range(0, 0, 0, 0), 'range')]; } })); + disposables.push(extHost.registerDocumentRangeFormattingEditProvider(defaultSelector, { + provideDocumentRangeFormattingEdits(): any { + return [new types.TextEdit(new types.Range(2, 3, 4, 5), 'range2')]; + } + })); disposables.push(extHost.registerDocumentFormattingEditProvider(defaultSelector, { provideDocumentFormattingEdits(): any { return [new types.TextEdit(new types.Range(0, 0, 1, 1), 'doc')]; @@ -1087,7 +1100,11 @@ suite('ExtHostLanguageFeatures', function () { return getDocumentRangeFormattingEdits(model, new EditorRange(1, 1, 1, 1), { insertSpaces: true, tabSize: 4 }).then(value => { assert.equal(value.length, 1); let [first] = value; - assert.equal(first.text, 'range'); + assert.equal(first.text, 'range2'); + assert.equal(first.range.startLineNumber, 3); + assert.equal(first.range.startColumn, 4); + assert.equal(first.range.endLineNumber, 5); + assert.equal(first.range.endColumn, 6); }); }); }); @@ -1166,4 +1183,26 @@ suite('ExtHostLanguageFeatures', function () { }); }); }); + + test('Document colors, data conversion', function () { + + disposables.push(extHost.registerColorProvider(defaultSelector, { + provideDocumentColors(): vscode.ColorInformation[] { + return [new types.ColorInformation(new types.Range(0, 0, 0, 20), new types.Color(0.1, 0.2, 0.3, 0.4))]; + }, + provideColorPresentations(color: vscode.Color, context: { range: vscode.Range, document: vscode.TextDocument }): vscode.ColorPresentation[] { + return []; + } + })); + + return rpcProtocol.sync().then(() => { + return getColors(model).then(value => { + assert.equal(value.length, 1); + let [first] = value; + + assert.deepEqual(first.colorInfo.color, { red: 0.1, green: 0.2, blue: 0.3, alpha: 0.4 }); + assert.deepEqual(first.colorInfo.range, { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 21 }); + }); + }); + }); }); diff --git a/src/vs/workbench/test/electron-browser/api/extHostMessagerService.test.ts b/src/vs/workbench/test/electron-browser/api/extHostMessagerService.test.ts index a7711e888f6..feeab0b2895 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostMessagerService.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostMessagerService.test.ts @@ -9,7 +9,7 @@ import * as assert from 'assert'; import { MainThreadMessageService } from 'vs/workbench/api/electron-browser/mainThreadMessageService'; import { TPromise as Promise, TPromise } from 'vs/base/common/winjs.base'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { INotificationService, INotification, NoOpNotification, INotificationHandle, PromptOption, Severity } from 'vs/platform/notification/common/notification'; +import { INotificationService, INotification, NoOpNotification, INotificationHandle, Severity, IPromptChoice } from 'vs/platform/notification/common/notification'; import { ICommandService } from 'vs/platform/commands/common/commands'; const emptyDialogService = new class implements IDialogService { @@ -45,7 +45,7 @@ const emptyNotificationService = new class implements INotificationService { error(...args: any[]): never { throw new Error('not implemented'); } - prompt(severity: Severity, message: string, choices: PromptOption[]): TPromise { + prompt(severity: Severity, message: string, choices: IPromptChoice[], onCancel?: () => void): INotificationHandle { throw new Error('not implemented'); } }; @@ -71,8 +71,8 @@ class EmptyNotificationService implements INotificationService { error(message: any): void { throw new Error('Method not implemented.'); } - prompt(severity: Severity, message: string, choices: PromptOption[]): Promise { - throw new Error('Method not implemented.'); + prompt(severity: Severity, message: string, choices: IPromptChoice[], onCancel?: () => void): INotificationHandle { + throw new Error('not implemented'); } } diff --git a/src/vs/workbench/test/electron-browser/api/extHostTreeViews.test.ts b/src/vs/workbench/test/electron-browser/api/extHostTreeViews.test.ts index 9b95fc29698..969cac30359 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostTreeViews.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostTreeViews.test.ts @@ -423,7 +423,7 @@ suite('ExtHostTreeView', function () { }); }); - test('reveal will return parents array for an element', () => { + test('reveal will return parents array for an element when hierarchy is not loaded', () => { const revealTarget = sinon.spy(target, '$reveal'); const treeView = testObject.createTreeView('treeDataProvider', { treeDataProvider: aCompleteNodeTreeDataProvider() }); return treeView.reveal({ key: 'aa' }) @@ -436,6 +436,21 @@ suite('ExtHostTreeView', function () { }); }); + test('reveal will return parents array for an element when hierarchy is loaded', () => { + const revealTarget = sinon.spy(target, '$reveal'); + const treeView = testObject.createTreeView('treeDataProvider', { treeDataProvider: aCompleteNodeTreeDataProvider() }); + return testObject.$getChildren('treeDataProvider') + .then(() => testObject.$getChildren('treeDataProvider', '0/0:a')) + .then(() => treeView.reveal({ key: 'aa' }) + .then(() => { + assert.ok(revealTarget.calledOnce); + assert.deepEqual('treeDataProvider', revealTarget.args[0][0]); + assert.deepEqual({ handle: '0/0:a/0:aa', label: 'aa', collapsibleState: TreeItemCollapsibleState.None, parentHandle: '0/0:a' }, removeUnsetKeys(revealTarget.args[0][1])); + assert.deepEqual([{ handle: '0/0:a', label: 'a', collapsibleState: TreeItemCollapsibleState.Collapsed }], (>revealTarget.args[0][2]).map(arg => removeUnsetKeys(arg))); + assert.equal(void 0, revealTarget.args[0][3]); + })); + }); + test('reveal will return parents array for deeper element with no selection', () => { tree = { 'b': { diff --git a/src/vs/workbench/test/electron-browser/api/extHostWebview.test.ts b/src/vs/workbench/test/electron-browser/api/extHostWebview.test.ts new file mode 100644 index 00000000000..0c600626db4 --- /dev/null +++ b/src/vs/workbench/test/electron-browser/api/extHostWebview.test.ts @@ -0,0 +1,62 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { MainThreadWebviews } from 'vs/workbench/api/electron-browser/mainThreadWebview'; +import { ExtHostWebviews } from 'vs/workbench/api/node/extHostWebview'; +import { mock } from 'vs/workbench/test/electron-browser/api/mock'; +import * as vscode from 'vscode'; +import { SingleProxyRPCProtocol } from './testRPCProtocol'; +import { Position as EditorPosition } from 'vs/platform/editor/common/editor'; + +suite('ExtHostWebview', function () { + + test('Cannot register multiple serializer for the same view type', async () => { + const viewType = 'view.type'; + + const shape = createNoopMainThreadWebviews(); + const extHostWebviews = new ExtHostWebviews(SingleProxyRPCProtocol(shape)); + + let lastInvokedDeserializer: vscode.WebviewPanelSerializer | undefined = undefined; + + class NoopSerializer implements vscode.WebviewPanelSerializer { + async serializeWebviewPanel(webview: vscode.WebviewPanel): Promise { /* noop */ } + + async deserializeWebviewPanel(webview: vscode.WebviewPanel, state: any): Promise { + lastInvokedDeserializer = this; + } + } + + const serializerA = new NoopSerializer(); + const serializerB = new NoopSerializer(); + + const serializerARegistration = extHostWebviews.registerWebviewPanelSerializer(viewType, serializerA); + + await extHostWebviews.$deserializeWebviewPanel('x', viewType, 'title', {}, EditorPosition.ONE, {}); + assert.strictEqual(lastInvokedDeserializer, serializerA); + + assert.throws( + () => extHostWebviews.registerWebviewPanelSerializer(viewType, serializerB), + 'Should throw when registering two serializers for the same view'); + + serializerARegistration.dispose(); + + extHostWebviews.registerWebviewPanelSerializer(viewType, serializerB); + + await extHostWebviews.$deserializeWebviewPanel('x', viewType, 'title', {}, EditorPosition.ONE, {}); + assert.strictEqual(lastInvokedDeserializer, serializerB); + }); +}); + + +function createNoopMainThreadWebviews() { + return new class extends mock() { + $registerSerializer() { /* noop */ } + $unregisterSerializer() { /* noop */ } + }; +} + diff --git a/src/vs/workbench/test/electron-browser/api/mainThreadSaveParticipant.test.ts b/src/vs/workbench/test/electron-browser/api/mainThreadSaveParticipant.test.ts index 995c2d814a8..f0465f76568 100644 --- a/src/vs/workbench/test/electron-browser/api/mainThreadSaveParticipant.test.ts +++ b/src/vs/workbench/test/electron-browser/api/mainThreadSaveParticipant.test.ts @@ -139,4 +139,31 @@ suite('MainThreadSaveParticipant', function () { }); }); + test('trim final new lines bug#46075', function () { + const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/trim_final_new_line.txt'), 'utf8'); + + return model.load().then(() => { + const configService = new TestConfigurationService(); + configService.setUserConfiguration('files', { 'trimFinalNewlines': true }); + + const participant = new TrimFinalNewLinesParticipant(configService, undefined); + + const textContent = 'Test'; + const eol = `${model.textEditorModel.getEOL()}`; + + let content = `${textContent}${eol}${eol}`; + model.textEditorModel.setValue(content); + // save many times + for (let i = 0; i < 10; i++) { + participant.participate(model, { reason: SaveReason.EXPLICIT }); + } + // confirm trimming + assert.equal(snapshotToString(model.createSnapshot()), `${textContent}${eol}`); + // undo should go back to previous content immediately + model.textEditorModel.undo(); + assert.equal(snapshotToString(model.createSnapshot()), `${textContent}${eol}${eol}`); + model.textEditorModel.redo(); + assert.equal(snapshotToString(model.createSnapshot()), `${textContent}${eol}`); + }); + }); }); diff --git a/src/vs/workbench/test/workbenchTestServices.ts b/src/vs/workbench/test/workbenchTestServices.ts index 545ded7f0a3..5acfb8aab9e 100644 --- a/src/vs/workbench/test/workbenchTestServices.ts +++ b/src/vs/workbench/test/workbenchTestServices.ts @@ -20,7 +20,7 @@ import Severity from 'vs/base/common/severity'; import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; -import { IPartService, Parts, Position as PartPosition, Dimension } from 'vs/workbench/services/part/common/partService'; +import { IPartService, Parts, Position as PartPosition, IDimension } from 'vs/workbench/services/part/common/partService'; import { TextModelResolverService } from 'vs/workbench/services/textmodelResolver/common/textModelResolverService'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { IEditorInput, IEditorOptions, Position, IEditor, IResourceInput } from 'vs/platform/editor/common/editor'; @@ -32,7 +32,7 @@ import { ServiceCollection } from 'vs/platform/instantiation/common/serviceColle import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService'; import { IEditorGroupService, GroupArrangement, GroupOrientation, IEditorTabOptions, IMoveOptions } from 'vs/workbench/services/group/common/groupService'; import { TextFileService } from 'vs/workbench/services/textfile/common/textFileService'; -import { FileOperationEvent, IFileService, IResolveContentOptions, FileOperationError, IFileStat, IResolveFileResult, IImportResult, FileChangesEvent, IResolveFileOptions, IContent, IUpdateContentOptions, IStreamContent, ICreateFileOptions, ITextSnapshot } from 'vs/platform/files/common/files'; +import { FileOperationEvent, IFileService, IResolveContentOptions, FileOperationError, IFileStat, IResolveFileResult, FileChangesEvent, IResolveFileOptions, IContent, IUpdateContentOptions, IStreamContent, ICreateFileOptions, ITextSnapshot, IResourceEncodings } from 'vs/platform/files/common/files'; import { IModelService } from 'vs/editor/common/services/modelService'; import { ModeServiceImpl } from 'vs/editor/common/services/modeServiceImpl'; import { ModelServiceImpl } from 'vs/editor/common/services/modelServiceImpl'; @@ -63,7 +63,8 @@ import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKe import { ITextBufferFactory, DefaultEndOfLine, EndOfLinePreference } from 'vs/editor/common/model'; import { Range } from 'vs/editor/common/core/range'; import { IConfirmation, IConfirmationResult, IDialogService, IDialogOptions } from 'vs/platform/dialogs/common/dialogs'; -import { INotificationService, INotificationHandle, INotification, NoOpNotification, PromptOption } from 'vs/platform/notification/common/notification'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; export function createFileInput(instantiationService: IInstantiationService, resource: URI): FileEditorInput { return instantiationService.createInstance(FileEditorInput, resource, void 0); @@ -257,6 +258,7 @@ export function workbenchInstantiationService(): IInstantiationService { instantiationService.stub(ITelemetryService, NullTelemetryService); instantiationService.stub(INotificationService, new TestNotificationService()); instantiationService.stub(IUntitledEditorService, instantiationService.createInstance(UntitledEditorService)); + instantiationService.stub(IWindowService, new TestWindowService()); instantiationService.stub(IWindowsService, new TestWindowsService()); instantiationService.stub(ITextFileService, instantiationService.createInstance(TestTextFileService)); instantiationService.stub(ITextModelService, instantiationService.createInstance(TextModelResolverService)); @@ -305,33 +307,6 @@ export class TestHistoryService implements IHistoryService { } } -export class TestNotificationService implements INotificationService { - - public _serviceBrand: any; - - private static readonly NO_OP: INotificationHandle = new NoOpNotification(); - - public info(message: string): INotificationHandle { - return this.notify({ severity: Severity.Info, message }); - } - - public warn(message: string): INotificationHandle { - return this.notify({ severity: Severity.Warning, message }); - } - - public error(error: string | Error): INotificationHandle { - return this.notify({ severity: Severity.Error, message: error }); - } - - public notify(notification: INotification): INotificationHandle { - return TestNotificationService.NO_OP; - } - - public prompt(severity: Severity, message: string, choices: PromptOption[]): TPromise { - return TPromise.as(0); - } -} - export class TestDialogService implements IDialogService { public _serviceBrand: any; @@ -350,13 +325,13 @@ export class TestPartService implements IPartService { public _serviceBrand: any; private _onTitleBarVisibilityChange = new Emitter(); - private _onEditorLayout = new Emitter(); + private _onEditorLayout = new Emitter(); public get onTitleBarVisibilityChange(): Event { return this._onTitleBarVisibilityChange.event; } - public get onEditorLayout(): Event { + public get onEditorLayout(): Event { return this._onEditorLayout.event; } @@ -679,6 +654,8 @@ export class TestFileService implements IFileService { public _serviceBrand: any; + public encoding: IResourceEncodings; + private readonly _onFileChanges: Emitter; private readonly _onAfterOperation: Emitter; @@ -796,8 +773,10 @@ export class TestFileService implements IFileService { return TPromise.as(null); } - touchFile(resource: URI): TPromise { - return TPromise.as(null); + onDidChangeFileSystemProviderRegistrations = Event.None; + + registerProvider(scheme: string, provider) { + return { dispose() { } }; } canHandleResource(resource: URI): boolean { @@ -808,20 +787,13 @@ export class TestFileService implements IFileService { return TPromise.as(null); } - importFile(source: URI, targetFolder: URI): TPromise { - return TPromise.as(null); - } - watchFileChanges(resource: URI): void { } unwatchFileChanges(resource: URI): void { } - updateOptions(options: any): void { - } - - getEncoding(resource: URI): string { + getWriteEncoding(resource: URI): string { return 'utf8'; } @@ -896,7 +868,7 @@ export class TestWindowService implements IWindowService { public _serviceBrand: any; - onDidChangeFocus: Event; + onDidChangeFocus: Event = new Emitter().event; isFocused(): TPromise { return TPromise.as(false); diff --git a/src/vs/workbench/workbench.main.ts b/src/vs/workbench/workbench.main.ts index 62d2302178b..f9cf799d7fe 100644 --- a/src/vs/workbench/workbench.main.ts +++ b/src/vs/workbench/workbench.main.ts @@ -19,6 +19,7 @@ import 'vs/editor/editor.all'; import 'vs/workbench/services/actions/electron-browser/menusExtensionPoint'; // Views +import 'vs/workbench/api/browser/viewsContainersExtensionPoint'; import 'vs/workbench/api/browser/viewsExtensionPoint'; // Localizations @@ -67,6 +68,7 @@ import 'vs/workbench/parts/markers/electron-browser/markers.contribution'; import 'vs/workbench/parts/html/electron-browser/html.contribution'; +import 'vs/workbench/parts/url/electron-browser/url.contribution'; import 'vs/workbench/parts/webview/electron-browser/webview.contribution'; import 'vs/workbench/parts/welcome/walkThrough/electron-browser/walkThrough.contribution'; @@ -86,8 +88,6 @@ import 'vs/workbench/parts/terminal/electron-browser/terminalPanel'; // can be p import 'vs/workbench/electron-browser/workbench'; -import 'vs/workbench/parts/trust/electron-browser/unsupportedWorkspaceSettings.contribution'; - import 'vs/workbench/parts/relauncher/electron-browser/relauncher.contribution'; import 'vs/workbench/parts/tasks/electron-browser/task.contribution'; diff --git a/test/electron/index.js b/test/electron/index.js index aa054f553ca..ee98b8f8ea3 100644 --- a/test/electron/index.js +++ b/test/electron/index.js @@ -8,7 +8,6 @@ const { tmpdir } = require('os'); const { join } = require('path'); const path = require('path'); const mocha = require('mocha'); -const JUnitReporter = require('mocha-junit-reporter'); const events = require('events'); const defaultReporterName = process.platform === 'win32' ? 'list' : 'spec'; @@ -132,7 +131,7 @@ app.on('ready', () => { win.webContents.on('did-finish-load', () => { if (argv.debug) { win.show(); - win.webContents.openDevTools('right'); + win.webContents.openDevTools({ mode: 'right' }); } win.webContents.send('run', argv); }); @@ -143,11 +142,6 @@ app.on('ready', () => { if (argv.tfs) { new TFSReporter(runner); - new JUnitReporter(runner, { - reporterOptions: { - mochaFile: '.build/tests/unit-test-results.xml' - } - }); } else { const reporterPath = path.join(path.dirname(require.resolve('mocha')), 'lib', 'reporters', argv.reporter); let Reporter; diff --git a/test/smoke/.gitignore b/test/smoke/.gitignore index 532798d3ea9..6601bb4c588 100644 --- a/test/smoke/.gitignore +++ b/test/smoke/.gitignore @@ -4,4 +4,5 @@ Thumbs.db node_modules/ out/ keybindings.*.json -test_data/ \ No newline at end of file +test_data/ +src/vscode/driver.d.ts \ No newline at end of file diff --git a/test/smoke/README.md b/test/smoke/README.md index 8856e454400..0647f5b9bbd 100644 --- a/test/smoke/README.md +++ b/test/smoke/README.md @@ -4,43 +4,26 @@ ``` # Dev -npm run smoketest +yarn smoketest -# Specific build -npm run smoketest -- --build "path/to/code" - -# Data Migration tests -npm run smoketest -- --build "path/to/code-insiders" --stable-build "path/to/code" +# Build +yarn smoketest --build "path/to/code" ``` -The script calls mocha, so all mocha arguments should work fine. For example, use `-f Git` to only run the `Git` tests. +The script calls mocha, so all mocha arguments should work fine. For example, use `-f Git` to filter all tests except the `Git` tests. -By default, screenshots are not captured. To run tests with screenshots use the argument `--screenshots`. +A `--verbose` flag can be used to log to the console all the low level driver calls make to Code. + +Screenshots can be captured when tests fail. In order to get them,you need to use the argument `--screenshots SCREENSHOT_DIR`. ## Pitfalls -- Beware of **state**. The tests within a single suite will share the same state. +- Beware of workbench **state**. The tests within a single suite will share the same state. - Beware of **singletons**. This evil can, and will, manifest itself under the form of FS paths, TCP ports, IPC handles. Whenever writing a test, or setting up more smoke test architecture, make sure it can run simultaneously with any other tests and even itself. All test suites should be able to run many times in parallel. - Beware of **focus**. **Never** depend on DOM elements having focus using `.focused` classes or `:focus` pseudo-classes, since they will lose that state as soon as another window appears on top of the running VS Code window. A safe approach which avoids this problem is to use the `waitForActiveElement` API. Many tests use this whenever they need to wait for a specific element to _have focus_. -- Beware of **timing**. You need to read from or write to the DOM... yeah I know. But is it the right time to do that? Can you 100% promise that that `input` box will be visible and in the DOM at this point in time? Or are you just hoping that it will be so? Every time you want to interact with the DOM, be absolutely sure that you can. Eg. just because you triggered Quick Open, it doesn't mean that it's open; you must wait for the widget to be in the DOM and for its input field to be the active element. +- Beware of **timing**. You need to read from or write to the DOM... but is it the right time to do that? Can you 100% guarantee that that `input` box will be visible at that point in time? Or are you just hoping that it will be so? Hope is your worst enemy in UI tests. Example: just because you triggered Quick Open with `F1`, it doesn't mean that it's open and you can just start typing; you must first wait for the input element to be in the DOM as well as be the current active element. -- Beware of **waiting**. **Never** wait longer than a couple of seconds for anything, unless it's justified. Think of it as a human using Code. Would a human take 10 minutes to run through the Search viewlet smoke test? Then, the computer should even be faster. **Don't** use `setTimeout` just because. Think about what you should wait for in the DOM to be ready, then wait for that instead. - -## Common Issues - -### Certain keys don't appear in input boxes (eg: Space) - -This is a **waiting** issue. Everytime you send keys to Code, you must be aware that the keybinding service can handle them. Even if you're sure that input box is focused. - -Here's an example: when opening quick open, focus goes from its list to its input. We used to simply wait for the input to have focus and then send some text to be typed, like `Workbench: Show Editor`; yet, only `Workbench:ShowEditor` would be rendered in the input box. This happened due to the fact that the [`ListService` takes 50ms to unset the context key which indicates a list is focused](https://github.com/Microsoft/vscode/blob/c8dee4c016d3a3d475011106e04d8e394d9f138c/src/vs/platform/list/browser/listService.ts#L59). The fix was to [wait 50ms as well on the smoke test](https://github.com/Microsoft/vscode/blob/b82fa8dcb06bbf9c85c1502d0d43322e2e9d1a59/test/smoke/src/areas/quickopen/quickopen.ts#L65). - -### I type in a Monaco editor instance, but the text doesn't appear to be there - -This is a **waiting** issue. When you type in a Monaco editor instance, you're really typing in a `textarea`. The `textarea` is then polled for its contents, then the editor model gets updated and finally the editor view gets updated. It's a good idea to always wait for the text to appear rendered in the editor after you type in it. - -### I type in a Monaco editor instance, but the text appears scrambled - -This is an issue which is **not yet fixed**. Unfortunately this seems to happen whenever the CPU load of the system is high. Rerunning the test will often result in a successful outcome. \ No newline at end of file +- Beware of **waiting**. **Never** wait longer than a couple of seconds for anything, unless it's justified. Think of it as a human using Code. Would a human take 10 minutes to run through the Search viewlet smoke test? Then, the computer should even be faster. **Don't** use `setTimeout` just because. Think about what you should wait for in the DOM to be ready and wait for that instead. diff --git a/test/smoke/package.json b/test/smoke/package.json index 97d8c0b3c3e..e033db34da0 100644 --- a/test/smoke/package.json +++ b/test/smoke/package.json @@ -3,8 +3,13 @@ "version": "0.1.0", "main": "./src/main.js", "scripts": { - "postinstall": "tsc", - "watch": "tsc --watch", + "postinstall": "npm run compile", + "compile": "npm run copy-driver && npm run copy-driver-definition && tsc", + "watch": "concurrently \"npm run watch-driver\" \"npm run watch-driver-definition\" \"tsc --watch\"", + "copy-driver": "cpx src/vscode/driver.js out/vscode", + "watch-driver": "cpx src/vscode/driver.js out/vscode -w", + "copy-driver-definition": "node tools/copy-driver-definition.js", + "watch-driver-definition": "watch \"node tools/copy-driver-definition.js\" ../../src/vs/platform/driver/common", "mocha": "mocha" }, "devDependencies": { @@ -15,6 +20,8 @@ "@types/node": "8.0.33", "@types/rimraf": "2.0.2", "@types/webdriverio": "4.6.1", + "concurrently": "^3.5.1", + "cpx": "^1.5.0", "electron": "1.7.7", "htmlparser2": "^3.9.2", "mkdirp": "^0.5.1", @@ -22,9 +29,9 @@ "ncp": "^2.0.0", "portastic": "^1.0.1", "rimraf": "^2.6.1", - "spectron": "^3.7.2", "strip-json-comments": "^2.0.1", "tmp": "0.0.33", - "typescript": "2.5.2" + "typescript": "2.5.2", + "watch": "^1.0.2" } -} \ No newline at end of file +} diff --git a/test/smoke/src/application.ts b/test/smoke/src/application.ts new file mode 100644 index 00000000000..cd63330d0ab --- /dev/null +++ b/test/smoke/src/application.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 { Workbench } from './areas/workbench/workbench'; +import * as fs from 'fs'; +import * as cp from 'child_process'; +import { Code, spawn, SpawnOptions } from './vscode/code'; +import { Logger } from './logger'; + +export enum Quality { + Dev, + Insiders, + Stable +} + +export interface ApplicationOptions extends SpawnOptions { + quality: Quality; + workspacePath: string; + workspaceFilePath: string; + waitTime: number; +} + +export class Application { + + private _code: Code | undefined; + private _workbench: Workbench; + private keybindings: any[]; + + constructor(private options: ApplicationOptions) { } + + get quality(): Quality { + return this.options.quality; + } + + get code(): Code { + return this._code!; + } + + get workbench(): Workbench { + return this._workbench; + } + + get logger(): Logger { + return this.options.logger; + } + + get workspacePath(): string { + return this.options.workspacePath; + } + + get extensionsPath(): string { + return this.options.extensionsPath; + } + + get userDataPath(): string { + return this.options.userDataDir; + } + + get workspaceFilePath(): string { + return this.options.workspaceFilePath; + } + + async start(): Promise { + await this._start(); + await this.code.waitForElement('.explorer-folders-view'); + await this.code.waitForActiveElement(`.editor-container[id="workbench.editor.walkThroughPart"] > div > div[tabIndex="0"]`); + } + + async restart(options: { workspaceOrFolder?: string, extraArgs?: string[] }): Promise { + await this.stop(); + await new Promise(c => setTimeout(c, 1000)); + await this._start(options.workspaceOrFolder, options.extraArgs); + } + + private async _start(workspaceOrFolder = this.options.workspacePath, extraArgs: string[] = []): Promise { + await this.retrieveKeybindings(); + cp.execSync('git checkout .', { cwd: this.options.workspacePath }); + await this.startApplication(workspaceOrFolder, extraArgs); + await this.checkWindowReady(); + } + + async reload(): Promise { + this.code.reload() + .catch(err => null); // ignore the connection drop errors + + // needs to be enough to propagate the 'Reload Window' command + await new Promise(c => setTimeout(c, 1500)); + await this.checkWindowReady(); + } + + async stop(): Promise { + if (this._code) { + this._code.dispose(); + this._code = undefined; + } + } + + async capturePage(): Promise { + return this.code.capturePage(); + } + + private async startApplication(workspaceOrFolder: string, extraArgs: string[] = []): Promise { + this._code = await spawn({ + codePath: this.options.codePath, + workspacePath: workspaceOrFolder, + userDataDir: this.options.userDataDir, + extensionsPath: this.options.extensionsPath, + logger: this.options.logger, + extraArgs + }); + + this._workbench = new Workbench(this._code, this.keybindings, this.userDataPath); + } + + private async checkWindowReady(): Promise { + if (!this.code) { + console.error('No code instance found'); + return; + } + + await this.code.waitForWindowIds(ids => ids.length > 0); + await this.code.waitForElement('.monaco-workbench'); + + // wait a bit, since focus might be stolen off widgets + // as soon as they open (eg quick open) + await new Promise(c => setTimeout(c, 500)); + } + + private retrieveKeybindings(): Promise { + return new Promise((c, e) => { + fs.readFile(process.env.VSCODE_KEYBINDINGS_PATH as string, 'utf8', (err, data) => { + if (err) { + throw err; + } + try { + this.keybindings = JSON.parse(data); + c(); + } catch (e) { + throw new Error(`Error parsing keybindings JSON: ${e}`); + } + }); + }); + } +} diff --git a/test/smoke/src/areas/activitybar/activityBar.ts b/test/smoke/src/areas/activitybar/activityBar.ts index 894eef36f9f..a2acb1ac44d 100644 --- a/test/smoke/src/areas/activitybar/activityBar.ts +++ b/test/smoke/src/areas/activitybar/activityBar.ts @@ -3,8 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Element } from 'webdriverio'; -import { SpectronApplication } from '../../spectron/application'; +import { Code } from '../../vscode/code'; export enum ActivityBarPosition { LEFT = 0, @@ -13,11 +12,9 @@ export enum ActivityBarPosition { export class ActivityBar { - constructor(private spectron: SpectronApplication) { - // noop - } + constructor(private code: Code) { } - public async getActivityBar(position: ActivityBarPosition): Promise { + async waitForActivityBar(position: ActivityBarPosition): Promise { let positionClass: string; if (position === ActivityBarPosition.LEFT) { @@ -28,6 +25,6 @@ export class ActivityBar { throw new Error('No such position for activity bar defined.'); } - return this.spectron.client.waitForElement(`.part.activitybar.${positionClass}`); + await this.code.waitForElement(`.part.activitybar.${positionClass}`); } } \ No newline at end of file diff --git a/test/smoke/src/areas/css/css.test.ts b/test/smoke/src/areas/css/css.test.ts index b7ee545445a..785de831bd6 100644 --- a/test/smoke/src/areas/css/css.test.ts +++ b/test/smoke/src/areas/css/css.test.ts @@ -3,55 +3,44 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; -import { SpectronApplication } from '../../spectron/application'; +import { Application } from '../../application'; import { ProblemSeverity, Problems } from '../problems/problems'; export function setup() { describe('CSS', () => { - before(function () { - this.app.suiteName = 'CSS'; - }); - it('verifies quick outline', async function () { - const app = this.app as SpectronApplication; + const app = this.app as Application; await app.workbench.quickopen.openFile('style.css'); - await app.workbench.editor.openOutline(); + await app.workbench.quickopen.openQuickOutline(); await app.workbench.quickopen.waitForQuickOpenElements(names => names.length === 2); }); it('verifies warnings for the empty rule', async function () { - const app = this.app as SpectronApplication; + const app = this.app as Application; await app.workbench.quickopen.openFile('style.css'); await app.workbench.editor.waitForTypeInEditor('style.css', '.foo{}'); - let warning = await app.client.waitForElement(Problems.getSelectorInEditor(ProblemSeverity.WARNING)); - await app.screenCapturer.capture('CSS Warning in editor'); - assert.ok(warning, `Warning squiggle is not shown in 'style.css'.`); + await app.code.waitForElement(Problems.getSelectorInEditor(ProblemSeverity.WARNING)); await app.workbench.problems.showProblemsView(); - warning = await app.client.waitForElement(Problems.getSelectorInProblemsView(ProblemSeverity.WARNING)); - await app.screenCapturer.capture('CSS Warning in problems view'); - assert.ok(warning, 'Warning does not appear in Problems view.'); + await app.code.waitForElement(Problems.getSelectorInProblemsView(ProblemSeverity.WARNING)); await app.workbench.problems.hideProblemsView(); }); it('verifies that warning becomes an error once setting changed', async function () { - const app = this.app as SpectronApplication; + // settings might take a while to update? + this.timeout(40000); + + const app = this.app as Application; await app.workbench.settingsEditor.addUserSetting('css.lint.emptyRules', '"error"'); await app.workbench.quickopen.openFile('style.css'); - await app.workbench.editor.waitForTypeInEditor('style.css', '.foo{}'); - let error = await app.client.waitForElement(Problems.getSelectorInEditor(ProblemSeverity.ERROR)); - await app.screenCapturer.capture('CSS Error in editor'); - assert.ok(error, `Warning squiggle is not shown in 'style.css'.`); + await app.code.waitForElement(Problems.getSelectorInEditor(ProblemSeverity.ERROR)); - const problems = new Problems(app); + const problems = new Problems(app.code, app.workbench); await problems.showProblemsView(); - error = await app.client.waitForElement(Problems.getSelectorInProblemsView(ProblemSeverity.ERROR)); - await app.screenCapturer.capture('CSS Error in probles view'); - assert.ok(error, 'Warning does not appear in Problems view.'); + await app.code.waitForElement(Problems.getSelectorInProblemsView(ProblemSeverity.ERROR)); await problems.hideProblemsView(); }); }); diff --git a/test/smoke/src/areas/debug/debug.test.ts b/test/smoke/src/areas/debug/debug.test.ts index 7e924b95b62..b193d856cfa 100644 --- a/test/smoke/src/areas/debug/debug.test.ts +++ b/test/smoke/src/areas/debug/debug.test.ts @@ -8,17 +8,12 @@ import * as http from 'http'; import * as path from 'path'; import * as fs from 'fs'; import * as stripJsonComments from 'strip-json-comments'; -import { SpectronApplication } from '../../spectron/application'; +import { Application } from '../../application'; export function setup() { describe('Debug', () => { - before(async function () { - const app = this.app as SpectronApplication; - app.suiteName = 'Debug'; - }); - it('configure launch json', async function () { - const app = this.app as SpectronApplication; + const app = this.app as Application; await app.workbench.debug.openDebugViewlet(); await app.workbench.quickopen.openFile('app.js'); @@ -31,7 +26,6 @@ export function setup() { fs.writeFileSync(launchJsonPath, JSON.stringify(config, undefined, 4), 'utf8'); await app.workbench.editor.waitForEditorContents('launch.json', contents => /"protocol": "inspector"/.test(contents)); - await app.screenCapturer.capture('launch.json file'); assert.equal(config.configurations[0].request, 'launch'); assert.equal(config.configurations[0].type, 'node'); @@ -43,66 +37,61 @@ export function setup() { }); it('breakpoints', async function () { - const app = this.app as SpectronApplication; + const app = this.app as Application; await app.workbench.quickopen.openFile('index.js'); await app.workbench.debug.setBreakpointOnLine(6); - await app.screenCapturer.capture('breakpoints are set'); }); let port: number; it('start debugging', async function () { - const app = this.app as SpectronApplication; + const app = this.app as Application; + + // TODO@isidor + await new Promise(c => setTimeout(c, 100)); port = await app.workbench.debug.startDebugging(); - await app.screenCapturer.capture('debugging has started'); await new Promise((c, e) => { const request = http.get(`http://localhost:${port}`); request.on('error', e); app.workbench.debug.waitForStackFrame(sf => sf.name === 'index.js' && sf.lineNumber === 6, 'looking for index.js and line 6').then(c, e); }); - - await app.screenCapturer.capture('debugging is paused'); }); it('focus stack frames and variables', async function () { - const app = this.app as SpectronApplication; + const app = this.app as Application; - await app.client.waitFor(() => app.workbench.debug.getLocalVariableCount(), c => c === 4, 'there should be 4 local variables'); + await app.workbench.debug.waitForVariableCount(4); await app.workbench.debug.focusStackFrame('layer.js', 'looking for layer.js'); - await app.client.waitFor(() => app.workbench.debug.getLocalVariableCount(), c => c === 5, 'there should be 5 local variables'); + await app.workbench.debug.waitForVariableCount(5); await app.workbench.debug.focusStackFrame('route.js', 'looking for route.js'); - await app.client.waitFor(() => app.workbench.debug.getLocalVariableCount(), c => c === 3, 'there should be 3 local variables'); + await app.workbench.debug.waitForVariableCount(3); await app.workbench.debug.focusStackFrame('index.js', 'looking for index.js'); - await app.client.waitFor(() => app.workbench.debug.getLocalVariableCount(), c => c === 4, 'there should be 4 local variables'); + await app.workbench.debug.waitForVariableCount(4); }); it('stepOver, stepIn, stepOut', async function () { - const app = this.app as SpectronApplication; + const app = this.app as Application; await app.workbench.debug.stepIn(); - await app.screenCapturer.capture('debugging has stepped in'); const first = await app.workbench.debug.waitForStackFrame(sf => sf.name === 'response.js', 'looking for response.js'); await app.workbench.debug.stepOver(); - await app.screenCapturer.capture('debugging has stepped over'); await app.workbench.debug.waitForStackFrame(sf => sf.name === 'response.js' && sf.lineNumber === first.lineNumber + 1, `looking for response.js and line ${first.lineNumber + 1}`); await app.workbench.debug.stepOut(); - await app.screenCapturer.capture('debugging has stepped out'); await app.workbench.debug.waitForStackFrame(sf => sf.name === 'index.js' && sf.lineNumber === 7, `looking for index.js and line 7`); }); it('continue', async function () { - const app = this.app as SpectronApplication; + const app = this.app as Application; await app.workbench.debug.continue(); - await app.screenCapturer.capture('debugging has continued'); await new Promise((c, e) => { const request = http.get(`http://localhost:${port}`); @@ -110,20 +99,18 @@ export function setup() { app.workbench.debug.waitForStackFrame(sf => sf.name === 'index.js' && sf.lineNumber === 6, `looking for index.js and line 6`).then(c, e); }); - await app.screenCapturer.capture('debugging is paused'); }); it('debug console', async function () { - const app = this.app as SpectronApplication; + const app = this.app as Application; await app.workbench.debug.waitForReplCommand('2 + 2', r => r === '4'); }); it('stop debugging', async function () { - const app = this.app as SpectronApplication; + const app = this.app as Application; await app.workbench.debug.stopDebugging(); - await app.screenCapturer.capture('debugging has stopped'); }); }); } \ No newline at end of file diff --git a/test/smoke/src/areas/debug/debug.ts b/test/smoke/src/areas/debug/debug.ts index 0f9dfa15582..bf92dd930ff 100644 --- a/test/smoke/src/areas/debug/debug.ts +++ b/test/smoke/src/areas/debug/debug.ts @@ -3,13 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { SpectronApplication } from '../../spectron/application'; import { Viewlet } from '../workbench/viewlet'; +import { Commands } from '../workbench/workbench'; +import { Code, findElement } from '../../vscode/code'; +import { Editors } from '../editor/editors'; +import { Editor } from '../editor/editor'; +import { IElement } from '../../vscode/driver'; const VIEWLET = 'div[id="workbench.view.debug"]'; const DEBUG_VIEW = `${VIEWLET} .debug-view-content`; const CONFIGURE = `div[id="workbench.parts.sidebar"] .actions-container .configure`; -const START = `.icon[title="Start Debugging"]`; const STOP = `.debug-actions-widget .debug-action.stop`; const STEP_OVER = `.debug-actions-widget .debug-action.step-over`; const STEP_IN = `.debug-actions-widget .debug-action.step-into`; @@ -22,150 +25,118 @@ const DEBUG_STATUS_BAR = `.statusbar.debugging`; const NOT_DEBUG_STATUS_BAR = `.statusbar:not(debugging)`; const TOOLBAR_HIDDEN = `.debug-actions-widget.monaco-builder-hidden`; const STACK_FRAME = `${VIEWLET} .monaco-tree-row .stack-frame`; +const SPECIFIC_STACK_FRAME = filename => `${STACK_FRAME} .file[title$="${filename}"]`; const VARIABLE = `${VIEWLET} .debug-variables .monaco-tree-row .expression`; -const CONSOLE_OUTPUT = `.repl .output.expression`; +const CONSOLE_OUTPUT = `.repl .output.expression .value`; const CONSOLE_INPUT_OUTPUT = `.repl .input-output-pair .output.expression .value`; const REPL_FOCUSED = '.repl-input-wrapper .monaco-editor textarea'; export interface IStackFrame { - id: string; name: string; lineNumber: number; } +function toStackFrame(element: IElement): IStackFrame { + const name = findElement(element, e => /\bfile-name\b/.test(e.className))!; + const line = findElement(element, e => /\bline-number\b/.test(e.className))!; + const lineNumber = line.textContent ? parseInt(line.textContent.split(':').shift() || '0') : 0; + + return { + name: name.textContent || '', + lineNumber + }; +} + export class Debug extends Viewlet { - constructor(spectron: SpectronApplication) { - super(spectron); + constructor(code: Code, private commands: Commands, private editors: Editors, private editor: Editor) { + super(code); } async openDebugViewlet(): Promise { - await this.spectron.runCommand('workbench.view.debug'); - await this.spectron.client.waitForElement(DEBUG_VIEW); + await this.commands.runCommand('workbench.view.debug'); + await this.code.waitForElement(DEBUG_VIEW); } async configure(): Promise { - await this.spectron.client.waitAndClick(CONFIGURE); - await this.spectron.workbench.waitForEditorFocus('launch.json'); + await this.code.waitAndClick(CONFIGURE); + await this.editors.waitForEditorFocus('launch.json'); } async setBreakpointOnLine(lineNumber: number): Promise { - await this.spectron.client.waitForElement(`${GLYPH_AREA}(${lineNumber})`); - await this.spectron.client.leftClick(`${GLYPH_AREA}(${lineNumber})`, 5, 5); - await this.spectron.client.waitForElement(BREAKPOINT_GLYPH); + await this.code.waitForElement(`${GLYPH_AREA}(${lineNumber})`); + await this.code.waitAndClick(`${GLYPH_AREA}(${lineNumber})`, 5, 5); + await this.code.waitForElement(BREAKPOINT_GLYPH); } async startDebugging(): Promise { - await this.spectron.client.waitAndClick(START); - await this.spectron.client.waitForElement(PAUSE); - await this.spectron.client.waitForElement(DEBUG_STATUS_BAR); + await this.commands.runCommand('workbench.action.debug.start'); + await this.code.waitForElement(PAUSE); + await this.code.waitForElement(DEBUG_STATUS_BAR); const portPrefix = 'Port: '; - await this.spectron.client.waitFor(async () => { - const output = await this.getConsoleOutput(); - return output.join(''); - }, text => !!text && text.indexOf(portPrefix) >= 0); - const output = await this.getConsoleOutput(); - const lastOutput = output.pop(); + + const output = await this.waitForOutput(output => output.some(line => line.indexOf(portPrefix) >= 0)); + const lastOutput = output.filter(line => line.indexOf(portPrefix) >= 0)[0]; return lastOutput ? parseInt(lastOutput.substr(portPrefix.length)) : 3000; } async stepOver(): Promise { - await this.spectron.client.waitAndClick(STEP_OVER); + await this.code.waitAndClick(STEP_OVER); } async stepIn(): Promise { - await this.spectron.client.waitAndClick(STEP_IN); + await this.code.waitAndClick(STEP_IN); } async stepOut(): Promise { - await this.spectron.client.waitAndClick(STEP_OUT); + await this.code.waitAndClick(STEP_OUT); } async continue(): Promise { - await this.spectron.client.waitAndClick(CONTINUE); + await this.code.waitAndClick(CONTINUE); await this.waitForStackFrameLength(0); } async stopDebugging(): Promise { - await this.spectron.client.waitAndClick(STOP); - await this.spectron.client.waitForElement(TOOLBAR_HIDDEN); - await this.spectron.client.waitForElement(NOT_DEBUG_STATUS_BAR); + await this.code.waitAndClick(STOP); + await this.code.waitForElement(TOOLBAR_HIDDEN); + await this.code.waitForElement(NOT_DEBUG_STATUS_BAR); } async waitForStackFrame(func: (stackFrame: IStackFrame) => boolean, message: string): Promise { - return await this.spectron.client.waitFor(async () => { - const stackFrames = await this.getStackFrames(); - return stackFrames.filter(func)[0]; - }, void 0, `Waiting for Stack Frame: ${message}`); + const elements = await this.code.waitForElements(STACK_FRAME, true, elements => elements.some(e => func(toStackFrame(e)))); + return elements.map(toStackFrame).filter(s => func(s))[0]; } async waitForStackFrameLength(length: number): Promise { - return await this.spectron.client.waitFor(() => this.getStackFrames(), stackFrames => stackFrames.length === length); + await this.code.waitForElements(STACK_FRAME, false, result => result.length === length); } async focusStackFrame(name: string, message: string): Promise { - const stackFrame = await this.waitForStackFrame(sf => sf.name === name, message); - await this.spectron.client.spectron.client.elementIdClick(stackFrame.id); - await this.spectron.workbench.waitForTab(name); + await this.code.waitAndClick(SPECIFIC_STACK_FRAME(name)); + await this.editors.waitForTab(name); } async waitForReplCommand(text: string, accept: (result: string) => boolean): Promise { - await this.spectron.workbench.quickopen.runCommand('Debug: Focus Debug Console'); - await this.spectron.client.waitForActiveElement(REPL_FOCUSED); - await this.spectron.client.setValue(REPL_FOCUSED, text); + await this.commands.runCommand('Debug: Focus Debug Console'); + await this.code.waitForActiveElement(REPL_FOCUSED); + await this.code.waitForSetValue(REPL_FOCUSED, text); // Wait for the keys to be picked up by the editor model such that repl evalutes what just got typed - await this.spectron.workbench.editor.waitForEditorContents('debug:input', s => s.indexOf(text) >= 0); - await this.spectron.client.keys(['Enter', 'NULL']); - await this.spectron.client.waitForElement(CONSOLE_INPUT_OUTPUT); - await this.spectron.client.waitFor(async () => { - const result = await this.getConsoleOutput(); - return result[result.length - 1] || ''; - }, accept); + await this.editor.waitForEditorContents('debug:input', s => s.indexOf(text) >= 0); + await this.code.dispatchKeybinding('enter'); + await this.code.waitForElement(CONSOLE_INPUT_OUTPUT); + await this.waitForOutput(output => accept(output[output.length - 1] || '')); } - async getLocalVariableCount(): Promise { - return await this.spectron.webclient.selectorExecute(VARIABLE, div => (Array.isArray(div) ? div : [div]).length); + async waitForVariableCount(count: number): Promise { + await this.code.waitForElements(VARIABLE, false, els => els.length === count); } - async getStackFramesLength(): Promise { - const stackFrames = await this.getStackFrames(); - return stackFrames.length; - } - - private async getStackFrames(): Promise { - const result = await this.spectron.webclient.selectorExecute(STACK_FRAME, - div => (Array.isArray(div) ? div : [div]).map(element => { - const name = element.querySelector('.file-name') as HTMLElement; - const line = element.querySelector('.line-number') as HTMLElement; - const lineNumber = line.textContent ? parseInt(line.textContent.split(':').shift() || '0') : 0; - - return { - name: name.textContent, - lineNumber, - element - }; - }) - ); - - if (!Array.isArray(result)) { - return []; - } - - return result - .map(({ name, lineNumber, element }) => ({ name, lineNumber, id: element.ELEMENT })); - } - - private async getConsoleOutput(): Promise { - const result = await this.spectron.webclient.selectorExecute(CONSOLE_OUTPUT, - div => (Array.isArray(div) ? div : [div]).map(element => { - const value = element.querySelector('.value') as HTMLElement; - return value && value.textContent; - }).filter(line => !!line) - ); - - return result; + private async waitForOutput(fn: (output: string[]) => boolean): Promise { + const elements = await this.code.waitForElements(CONSOLE_OUTPUT, false, elements => fn(elements.map(e => e.textContent))); + return elements.map(e => e.textContent); } } diff --git a/test/smoke/src/areas/editor/editor.test.ts b/test/smoke/src/areas/editor/editor.test.ts index 0911d100100..af9f49cf4c6 100644 --- a/test/smoke/src/areas/editor/editor.test.ts +++ b/test/smoke/src/areas/editor/editor.test.ts @@ -3,27 +3,23 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { SpectronApplication } from '../../spectron/application'; +import { Application } from '../../application'; export function setup() { describe('Editor', () => { - before(function () { - this.app.suiteName = 'Editor'; - }); - it('shows correct quick outline', async function () { - const app = this.app as SpectronApplication; + const app = this.app as Application; await app.workbench.quickopen.openFile('www'); - await app.workbench.editor.openOutline(); + await app.workbench.quickopen.openQuickOutline(); await app.workbench.quickopen.waitForQuickOpenElements(names => names.length >= 6); }); it(`finds 'All References' to 'app'`, async function () { - const app = this.app as SpectronApplication; + const app = this.app as Application; await app.workbench.quickopen.openFile('www'); - const references = await app.workbench.editor.findReferences('app', 7); + const references = await app.workbench.editor.findReferences('www', 'app', 7); await references.waitForReferencesCountInTitle(3); await references.waitForReferencesCount(3); @@ -31,11 +27,10 @@ export function setup() { }); it(`renames local 'app' variable`, async function () { - const app = this.app as SpectronApplication; + const app = this.app as Application; await app.workbench.quickopen.openFile('www'); await app.workbench.editor.rename('www', 7, 'app', 'newApp'); await app.workbench.editor.waitForEditorContents('www', contents => contents.indexOf('newApp') > -1); - await app.screenCapturer.capture('Rename result'); }); // it('folds/unfolds the code correctly', async function () { @@ -55,19 +50,19 @@ export function setup() { // }); it(`verifies that 'Go To Definition' works`, async function () { - const app = this.app as SpectronApplication; + const app = this.app as Application; await app.workbench.quickopen.openFile('app.js'); - await app.workbench.editor.gotoDefinition('express', 11); + await app.workbench.editor.gotoDefinition('app.js', 'express', 11); - await app.workbench.waitForActiveTab('index.d.ts'); + await app.workbench.editors.waitForActiveTab('index.d.ts'); }); it(`verifies that 'Peek Definition' works`, async function () { - const app = this.app as SpectronApplication; + const app = this.app as Application; await app.workbench.quickopen.openFile('app.js'); - const peek = await app.workbench.editor.peekDefinition('express', 11); + const peek = await app.workbench.editor.peekDefinition('app.js', 'express', 11); await peek.waitForFile('index.d.ts'); }); diff --git a/test/smoke/src/areas/editor/editor.ts b/test/smoke/src/areas/editor/editor.ts index 14c95e3ff6b..c3facfe1718 100644 --- a/test/smoke/src/areas/editor/editor.ts +++ b/test/smoke/src/areas/editor/editor.ts @@ -3,192 +3,131 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { SpectronApplication } from '../../spectron/application'; -import { QuickOutline } from './quickoutline'; import { References } from './peek'; +import { Commands } from '../workbench/workbench'; +import { Code } from '../../vscode/code'; const RENAME_BOX = '.monaco-editor .monaco-editor.rename-box'; const RENAME_INPUT = `${RENAME_BOX} .rename-input`; +const EDITOR = filename => `.monaco-editor[data-uri$="${filename}"]`; +const VIEW_LINES = filename => `${EDITOR(filename)} .view-lines`; +const LINE_NUMBERS = filename => `${EDITOR(filename)} .margin .margin-view-overlays .line-numbers`; export class Editor { - private static readonly VIEW_LINES = '.monaco-editor .view-lines'; - private static readonly LINE_NUMBERS = '.monaco-editor .margin .margin-view-overlays .line-numbers'; private static readonly FOLDING_EXPANDED = '.monaco-editor .margin .margin-view-overlays>:nth-child(${INDEX}) .folding'; private static readonly FOLDING_COLLAPSED = `${Editor.FOLDING_EXPANDED}.collapsed`; - constructor(private spectron: SpectronApplication) { - } + constructor(private code: Code, private commands: Commands) { } - async openOutline(): Promise { - const outline = new QuickOutline(this.spectron); - await outline.open(); - return outline; - } - - async findReferences(term: string, line: number): Promise { - await this.clickOnTerm(term, line); - await this.spectron.workbench.quickopen.runCommand('Find All References'); - const references = new References(this.spectron); + async findReferences(filename: string, term: string, line: number): Promise { + await this.clickOnTerm(filename, term, line); + await this.commands.runCommand('Find All References'); + const references = new References(this.code); await references.waitUntilOpen(); return references; } async rename(filename: string, line: number, from: string, to: string): Promise { - await this.clickOnTerm(from, line); - await this.spectron.workbench.quickopen.runCommand('Rename Symbol'); + await this.clickOnTerm(filename, from, line); + await this.commands.runCommand('Rename Symbol'); - await this.spectron.client.waitForActiveElement(RENAME_INPUT); - await this.spectron.client.setValue(RENAME_INPUT, to); + await this.code.waitForActiveElement(RENAME_INPUT); + await this.code.waitForSetValue(RENAME_INPUT, to); - await this.spectron.client.keys(['Enter', 'NULL']); + await this.code.dispatchKeybinding('enter'); } - async gotoDefinition(term: string, line: number): Promise { - await this.clickOnTerm(term, line); - await this.spectron.workbench.quickopen.runCommand('Go to Definition'); + async gotoDefinition(filename: string, term: string, line: number): Promise { + await this.clickOnTerm(filename, term, line); + await this.commands.runCommand('Go to Definition'); } - async peekDefinition(term: string, line: number): Promise { - await this.clickOnTerm(term, line); - await this.spectron.workbench.quickopen.runCommand('Peek Definition'); - const peek = new References(this.spectron); + async peekDefinition(filename: string, term: string, line: number): Promise { + await this.clickOnTerm(filename, term, line); + await this.commands.runCommand('Peek Definition'); + const peek = new References(this.code); await peek.waitUntilOpen(); return peek; } - async waitForHighlightingLine(line: number): Promise { - const currentLineIndex = await this.getViewLineIndex(line); + async waitForHighlightingLine(filename: string, line: number): Promise { + const currentLineIndex = await this.getViewLineIndex(filename, line); if (currentLineIndex) { - await this.spectron.client.waitForElement(`.monaco-editor .view-overlays>:nth-child(${currentLineIndex}) .current-line`); + await this.code.waitForElement(`.monaco-editor .view-overlays>:nth-child(${currentLineIndex}) .current-line`); return; } throw new Error('Cannot find line ' + line); } - async getSelector(term: string, line: number): Promise { - const lineIndex = await this.getViewLineIndex(line); - const classNames = await this.spectron.client.waitFor(() => this.getClassSelectors(term, lineIndex), classNames => classNames && !!classNames.length, 'Getting class names for editor lines'); - return `${Editor.VIEW_LINES}>:nth-child(${lineIndex}) span span.${classNames[0]}`; + private async getSelector(filename: string, term: string, line: number): Promise { + const lineIndex = await this.getViewLineIndex(filename, line); + const classNames = await this.getClassSelectors(filename, term, lineIndex); + + return `${VIEW_LINES(filename)}>:nth-child(${lineIndex}) span span.${classNames[0]}`; } - async foldAtLine(line: number): Promise { - const lineIndex = await this.getViewLineIndex(line); - await this.spectron.client.waitAndClick(Editor.FOLDING_EXPANDED.replace('${INDEX}', '' + lineIndex)); - await this.spectron.client.waitForElement(Editor.FOLDING_COLLAPSED.replace('${INDEX}', '' + lineIndex)); + async foldAtLine(filename: string, line: number): Promise { + const lineIndex = await this.getViewLineIndex(filename, line); + await this.code.waitAndClick(Editor.FOLDING_EXPANDED.replace('${INDEX}', '' + lineIndex)); + await this.code.waitForElement(Editor.FOLDING_COLLAPSED.replace('${INDEX}', '' + lineIndex)); } - async unfoldAtLine(line: number): Promise { - const lineIndex = await this.getViewLineIndex(line); - await this.spectron.client.waitAndClick(Editor.FOLDING_COLLAPSED.replace('${INDEX}', '' + lineIndex)); - await this.spectron.client.waitForElement(Editor.FOLDING_EXPANDED.replace('${INDEX}', '' + lineIndex)); + async unfoldAtLine(filename: string, line: number): Promise { + const lineIndex = await this.getViewLineIndex(filename, line); + await this.code.waitAndClick(Editor.FOLDING_COLLAPSED.replace('${INDEX}', '' + lineIndex)); + await this.code.waitForElement(Editor.FOLDING_EXPANDED.replace('${INDEX}', '' + lineIndex)); } - async waitUntilHidden(line: number): Promise { - await this.spectron.client.waitFor(() => this.getViewLineIndexWithoutWait(line), lineNumber => lineNumber === undefined, 'Waiting until line number is hidden'); + private async clickOnTerm(filename: string, term: string, line: number): Promise { + const selector = await this.getSelector(filename, term, line); + await this.code.waitAndClick(selector); } - async waitUntilShown(line: number): Promise { - await this.getViewLineIndex(line); - } + async waitForEditorFocus(filename: string, lineNumber: number, selectorPrefix = ''): Promise { + const editor = [selectorPrefix || '', EDITOR(filename)].join(' '); + const line = `${editor} .view-lines > .view-line:nth-child(${lineNumber})`; + const textarea = `${editor} textarea`; - async clickOnTerm(term: string, line: number): Promise { - const selector = await this.getSelector(term, line); - await this.spectron.client.waitAndClick(selector); + await this.code.waitAndClick(line, 0, 0); + await this.code.waitForActiveElement(textarea); } async waitForTypeInEditor(filename: string, text: string, selectorPrefix = ''): Promise { - const editor = [ - selectorPrefix || '', - `.monaco-editor[data-uri$="${filename}"]` - ].join(' '); + const editor = [selectorPrefix || '', EDITOR(filename)].join(' '); - await this.spectron.client.element(editor); + await this.code.waitForElement(editor); const textarea = `${editor} textarea`; - await this.spectron.client.waitForActiveElement(textarea); + await this.code.waitForActiveElement(textarea); - // https://github.com/Microsoft/vscode/issues/34203#issuecomment-334441786 - await this.spectron.client.spectron.client.selectorExecute(textarea, (elements, text) => { - const textarea = (Array.isArray(elements) ? elements : [elements])[0] as HTMLTextAreaElement; - const start = textarea.selectionStart; - const newStart = start + text.length; - const value = textarea.value; - const newValue = value.substr(0, start) + text + value.substr(start); - - textarea.value = newValue; - textarea.setSelectionRange(newStart, newStart); - - const event = new Event('input', { 'bubbles': true, 'cancelable': true }); - textarea.dispatchEvent(event); - }, text); + await this.code.waitForTypeInEditor(textarea, text); await this.waitForEditorContents(filename, c => c.indexOf(text) > -1, selectorPrefix); } async waitForEditorContents(filename: string, accept: (contents: string) => boolean, selectorPrefix = ''): Promise { - const selector = [ - selectorPrefix || '', - `.monaco-editor[data-uri$="${filename}"] .view-lines` - ].join(' '); - - return this.spectron.client.waitForTextContent(selector, undefined, c => accept(c.replace(/\u00a0/g, ' '))); + const selector = [selectorPrefix || '', `${EDITOR(filename)} .view-lines`].join(' '); + return this.code.waitForTextContent(selector, undefined, c => accept(c.replace(/\u00a0/g, ' '))); } - async waitForActiveEditor(filename: string): Promise { - const selector = `.editor-container .monaco-editor[data-uri$="${filename}"] textarea`; - return this.spectron.client.waitForActiveElement(selector); + private async getClassSelectors(filename: string, term: string, viewline: number): Promise { + const elements = await this.code.waitForElements(`${VIEW_LINES(filename)}>:nth-child(${viewline}) span span`, false, els => els.some(el => el.textContent === term)); + const { className } = elements.filter(r => r.textContent === term)[0]; + return className.split(/\s/g); } - // async waitForActiveEditorFirstLineText(filename: string): Promise { - // const selector = `.editor-container .monaco-editor[data-uri$="${filename}"] textarea`; - // const result = await this.spectron.client.waitFor( - // () => this.spectron.client.spectron.client.execute(s => { - // if (!document.activeElement.matches(s)) { - // return undefined; - // } + private async getViewLineIndex(filename: string, line: number): Promise { + const elements = await this.code.waitForElements(LINE_NUMBERS(filename), false, els => { + return els.some(el => el.textContent === `${line}`); + }); - // let element: Element | null = document.activeElement; - // while (element && !/monaco-editor/.test(element.className) && element !== document.body) { - // element = element.parentElement; - // } - - // if (element && /monaco-editor/.test(element.className)) { - // const firstLine = element.querySelector('.view-lines span span:nth-child(1)'); - - // if (firstLine) { - // return (firstLine.textContent || '').replace(/\u00a0/g, ' '); // DAMN - // } - // } - - // return undefined; - // }, selector), - // r => typeof r.value === 'string', - // `wait for active editor first line: ${selector}` - // ); - - // return result.value; - // } - - private async getClassSelectors(term: string, viewline: number): Promise { - const result: { text: string, className: string }[] = await this.spectron.webclient.selectorExecute(`${Editor.VIEW_LINES}>:nth-child(${viewline}) span span`, - elements => (Array.isArray(elements) ? elements : [elements]) - .map(element => ({ text: element.textContent, className: element.className }))); - return result.filter(r => r.text === term).map(({ className }) => className); - } - - private async getViewLineIndex(line: number): Promise { - return await this.spectron.client.waitFor(() => this.getViewLineIndexWithoutWait(line), void 0, 'Getting line index'); - } - - private async getViewLineIndexWithoutWait(line: number): Promise { - const lineNumbers = await this.spectron.webclient.selectorExecute(Editor.LINE_NUMBERS, - elements => (Array.isArray(elements) ? elements : [elements]).map(element => element.textContent)); - for (let index = 0; index < lineNumbers.length; index++) { - if (lineNumbers[index] === `${line}`) { + for (let index = 0; index < elements.length; index++) { + if (elements[index].textContent === `${line}`) { return index + 1; } } - return undefined; + + throw new Error('Line not found'); } } \ No newline at end of file diff --git a/test/smoke/src/areas/editor/editors.ts b/test/smoke/src/areas/editor/editors.ts new file mode 100644 index 00000000000..4ac026780e4 --- /dev/null +++ b/test/smoke/src/areas/editor/editors.ts @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Commands } from '../workbench/workbench'; +import { Code } from '../../vscode/code'; + +export class Editors { + + constructor(private code: Code, private commands: Commands) { } + + async saveOpenedFile(): Promise { + await this.commands.runCommand('workbench.action.files.save'); + } + + async selectTab(tabName: string, untitled: boolean = false): Promise { + await this.code.waitAndClick(`.tabs-container div.tab[aria-label="${tabName}, tab"]`); + await this.waitForEditorFocus(tabName, untitled); + } + + async waitForActiveEditor(filename: string): Promise { + const selector = `.editor-container .monaco-editor[data-uri$="${filename}"] textarea`; + return this.code.waitForActiveElement(selector); + } + + async waitForEditorFocus(fileName: string, untitled: boolean = false): Promise { + await this.waitForActiveTab(fileName); + await this.waitForActiveEditor(fileName); + } + + async waitForActiveTab(fileName: string, isDirty: boolean = false): Promise { + await this.code.waitForElement(`.tabs-container div.tab.active${isDirty ? '.dirty' : ''}[aria-selected="true"][aria-label="${fileName}, tab"]`); + } + + async waitForTab(fileName: string, isDirty: boolean = false): Promise { + await this.code.waitForElement(`.tabs-container div.tab${isDirty ? '.dirty' : ''}[aria-label="${fileName}, tab"]`); + } + + async newUntitledFile(): Promise { + await this.commands.runCommand('workbench.action.files.newUntitledFile'); + await this.waitForEditorFocus('Untitled-1', true); + } +} \ No newline at end of file diff --git a/test/smoke/src/areas/editor/peek.ts b/test/smoke/src/areas/editor/peek.ts index 9c912821270..e0f805b441e 100644 --- a/test/smoke/src/areas/editor/peek.ts +++ b/test/smoke/src/areas/editor/peek.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { SpectronApplication } from '../../spectron/application'; +import { Code } from '../../vscode/code'; export class References { @@ -12,30 +12,41 @@ export class References { private static readonly REFERENCES_TITLE_COUNT = `${References.REFERENCES_WIDGET} .head .peekview-title .meta`; private static readonly REFERENCES = `${References.REFERENCES_WIDGET} .body .ref-tree.inline .monaco-tree-row .reference`; - constructor(private spectron: SpectronApplication) { + constructor(private code: Code) { } + + async waitUntilOpen(): Promise { + await this.code.waitForElement(References.REFERENCES_WIDGET); } - public async waitUntilOpen(): Promise { - await this.spectron.client.waitForElement(References.REFERENCES_WIDGET); - } - - public async waitForReferencesCountInTitle(count: number): Promise { - await this.spectron.client.waitForText(References.REFERENCES_TITLE_COUNT, void 0, titleCount => { + async waitForReferencesCountInTitle(count: number): Promise { + await this.code.waitForTextContent(References.REFERENCES_TITLE_COUNT, void 0, titleCount => { const matches = titleCount.match(/\d+/); return matches ? parseInt(matches[0]) === count : false; }); } - public async waitForReferencesCount(count: number): Promise { - await this.spectron.client.waitForElements(References.REFERENCES, result => result && result.length === count); + async waitForReferencesCount(count: number): Promise { + await this.code.waitForElements(References.REFERENCES, false, result => result && result.length === count); } - public async waitForFile(file: string): Promise { - await this.spectron.client.waitForText(References.REFERENCES_TITLE_FILE_NAME, file); + async waitForFile(file: string): Promise { + await this.code.waitForTextContent(References.REFERENCES_TITLE_FILE_NAME, file); } - public async close(): Promise { - await this.spectron.client.keys(['Escape', 'NULL']); - await this.spectron.client.waitForElement(References.REFERENCES_WIDGET, element => !element); + async close(): Promise { + // Sometimes someone else eats up the `Escape` key + let count = 0; + while (true) { + await this.code.dispatchKeybinding('escape'); + + try { + await this.code.waitForElement(References.REFERENCES_WIDGET, el => !el, 10); + return; + } catch (err) { + if (++count > 5) { + throw err; + } + } + } } } \ No newline at end of file diff --git a/test/smoke/src/areas/editor/quickoutline.ts b/test/smoke/src/areas/editor/quickoutline.ts deleted file mode 100644 index 852bd8b9145..00000000000 --- a/test/smoke/src/areas/editor/quickoutline.ts +++ /dev/null @@ -1,28 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { SpectronApplication } from '../../spectron/application'; -import { QuickOpen } from '../quickopen/quickopen'; - -export class QuickOutline extends QuickOpen { - - constructor(spectron: SpectronApplication) { - super(spectron); - } - - public async open(): Promise { - await this.spectron.client.waitFor(async () => { - await this.spectron.runCommand('workbench.action.gotoSymbol'); - const entry = await this.spectron.client.element('div[aria-label="Quick Picker"] .monaco-tree-rows.show-twisties div.monaco-tree-row .quick-open-entry'); - if (entry) { - const text = await this.spectron.client.getText('div[aria-label="Quick Picker"] .monaco-tree-rows.show-twisties div.monaco-tree-row .quick-open-entry .monaco-icon-label .label-name .monaco-highlighted-label span'); - if (text !== 'No symbol information for the file') { - return entry; - } - } - await this.closeQuickOpen(); - }, undefined, 'Opening Outline'); - } -} diff --git a/test/smoke/src/areas/explorer/explorer.test.ts b/test/smoke/src/areas/explorer/explorer.test.ts index ab77642f6dd..946105649b7 100644 --- a/test/smoke/src/areas/explorer/explorer.test.ts +++ b/test/smoke/src/areas/explorer/explorer.test.ts @@ -3,16 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { SpectronApplication } from '../../spectron/application'; +import { Application } from '../../application'; export function setup() { describe('Explorer', () => { - before(function () { - this.app.suiteName = 'Explorer'; - }); - it('quick open search produces correct result', async function () { - const app = this.app as SpectronApplication; + const app = this.app as Application; const expectedNames = [ '.eslintrc.json', 'tasks.json', @@ -25,11 +21,11 @@ export function setup() { await app.workbench.quickopen.openQuickOpen('.js'); await app.workbench.quickopen.waitForQuickOpenElements(names => expectedNames.every(n => names.some(m => n === m))); - await app.client.keys(['Escape', 'NULL']); + await app.code.dispatchKeybinding('escape'); }); it('quick open respects fuzzy matching', async function () { - const app = this.app as SpectronApplication; + const app = this.app as Application; const expectedNames = [ 'tasks.json', 'app.js', @@ -38,7 +34,7 @@ export function setup() { await app.workbench.quickopen.openQuickOpen('a.s'); await app.workbench.quickopen.waitForQuickOpenElements(names => expectedNames.every(n => names.some(m => n === m))); - await app.client.keys(['Escape', 'NULL']); + await app.code.dispatchKeybinding('escape'); }); }); } \ No newline at end of file diff --git a/test/smoke/src/areas/explorer/explorer.ts b/test/smoke/src/areas/explorer/explorer.ts index 6159e62c45f..ef9231393ef 100644 --- a/test/smoke/src/areas/explorer/explorer.ts +++ b/test/smoke/src/areas/explorer/explorer.ts @@ -3,33 +3,34 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { SpectronApplication } from '../../spectron/application'; import { Viewlet } from '../workbench/viewlet'; - +import { Editors } from '../editor/editors'; +import { Commands } from '../workbench/workbench'; +import { Code } from '../../vscode/code'; export class Explorer extends Viewlet { private static readonly EXPLORER_VIEWLET = 'div[id="workbench.view.explorer"]'; private static readonly OPEN_EDITORS_VIEW = `${Explorer.EXPLORER_VIEWLET} .split-view-view:nth-child(1) .title`; - constructor(spectron: SpectronApplication) { - super(spectron); + constructor(code: Code, private commands: Commands, private editors: Editors) { + super(code); } - public openExplorerView(): Promise { - return this.spectron.runCommand('workbench.view.explorer'); + openExplorerView(): Promise { + return this.commands.runCommand('workbench.view.explorer'); } - public getOpenEditorsViewTitle(): Promise { - return this.spectron.client.waitForText(Explorer.OPEN_EDITORS_VIEW); + async waitForOpenEditorsViewTitle(fn: (title: string) => boolean): Promise { + await this.code.waitForTextContent(Explorer.OPEN_EDITORS_VIEW, undefined, fn); } - public async openFile(fileName: string): Promise { - await this.spectron.client.doubleClickAndWait(`div[class="monaco-icon-label file-icon ${fileName}-name-file-icon ${this.getExtensionSelector(fileName)} explorer-item"]`); - await this.spectron.workbench.waitForEditorFocus(fileName); + async openFile(fileName: string): Promise { + await this.code.waitAndDoubleClick(`div[class="monaco-icon-label file-icon ${fileName}-name-file-icon ${this.getExtensionSelector(fileName)} explorer-item"]`); + await this.editors.waitForEditorFocus(fileName); } - public getExtensionSelector(fileName: string): string { + getExtensionSelector(fileName: string): string { const extension = fileName.split('.')[1]; if (extension === 'js') { return 'js-ext-file-icon ext-file-icon javascript-lang-file-icon'; diff --git a/test/smoke/src/areas/extensions/extensions.test.ts b/test/smoke/src/areas/extensions/extensions.test.ts index 33776a25405..2b51741cd52 100644 --- a/test/smoke/src/areas/extensions/extensions.test.ts +++ b/test/smoke/src/areas/extensions/extensions.test.ts @@ -3,17 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; -import { SpectronApplication, Quality } from '../../spectron/application'; +import { Application, Quality } from '../../application'; export function setup() { describe('Extensions', () => { - before(function () { - this.app.suiteName = 'Extensions'; - }); - it(`install and activate vscode-smoketest-check extension`, async function () { - const app = this.app as SpectronApplication; + const app = this.app as Application; if (app.quality === Quality.Dev) { this.skip(); @@ -23,16 +18,12 @@ export function setup() { const extensionName = 'vscode-smoketest-check'; await app.workbench.extensions.openExtensionsViewlet(); - const installed = await app.workbench.extensions.installExtension(extensionName); - assert.ok(installed); + await app.workbench.extensions.installExtension(extensionName); await app.reload(); - await app.workbench.extensions.waitForExtensionsViewlet(); - await app.workbench.quickopen.runCommand('Smoke Test Check'); - - const statusbarText = await app.workbench.statusbar.getStatusbarTextByTitle('smoke test'); - await app.screenCapturer.capture('Statusbar'); - assert.equal(statusbarText, 'VS Code Smoke Test Check'); + await app.workbench.extensions.openExtensionsViewlet(); + await app.workbench.runCommand('Smoke Test Check'); + await app.workbench.statusbar.waitForStatusbarText('smoke test', 'VS Code Smoke Test Check'); }); }); } \ No newline at end of file diff --git a/test/smoke/src/areas/extensions/extensions.ts b/test/smoke/src/areas/extensions/extensions.ts index d0c3f97320d..a523250e928 100644 --- a/test/smoke/src/areas/extensions/extensions.ts +++ b/test/smoke/src/areas/extensions/extensions.ts @@ -3,40 +3,32 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { SpectronApplication } from '../../spectron/application'; import { Viewlet } from '../workbench/viewlet'; +import { Commands } from '../workbench/workbench'; +import { Code } from '../../vscode/code'; const SEARCH_BOX = 'div.extensions-viewlet[id="workbench.view.extensions"] input.search-box'; export class Extensions extends Viewlet { - constructor(spectron: SpectronApplication) { - super(spectron); + constructor(code: Code, private commands: Commands) { + super(code); } async openExtensionsViewlet(): Promise { - await this.spectron.runCommand('workbench.view.extensions'); - await this.waitForExtensionsViewlet(); - } - - async waitForExtensionsViewlet(): Promise { - await this.spectron.client.waitForActiveElement(SEARCH_BOX); + await this.commands.runCommand('workbench.view.extensions'); + await this.code.waitForActiveElement(SEARCH_BOX); } async searchForExtension(name: string): Promise { - await this.spectron.client.click(SEARCH_BOX); - await this.spectron.client.waitForActiveElement(SEARCH_BOX); - await this.spectron.client.setValue(SEARCH_BOX, name); + await this.code.waitAndClick(SEARCH_BOX); + await this.code.waitForActiveElement(SEARCH_BOX); + await this.code.waitForSetValue(SEARCH_BOX, `name:"${name}"`); } - async installExtension(name: string): Promise { + async installExtension(name: string): Promise { await this.searchForExtension(name); - - // we might want to wait for a while longer since the Marketplace can be slow - // a minute should do - await this.spectron.client.waitFor(() => this.spectron.client.click(`div.extensions-viewlet[id="workbench.view.extensions"] .monaco-list-row[aria-label="${name}"] .extension li[class='action-item'] .extension-action.install`), void 0, 'waiting for install button', 600); - - await this.spectron.client.waitForElement(`div.extensions-viewlet[id="workbench.view.extensions"] .monaco-list-row[aria-label="${name}"] .extension li[class='action-item'] .extension-action.reload`); - return true; + await this.code.waitAndClick(`div.extensions-viewlet[id="workbench.view.extensions"] .monaco-list-row[aria-label="${name}"] .extension li[class='action-item'] .extension-action.install`); + await this.code.waitForElement(`div.extensions-viewlet[id="workbench.view.extensions"] .monaco-list-row[aria-label="${name}"] .extension li[class='action-item'] .extension-action.reload`); } } \ No newline at end of file diff --git a/test/smoke/src/areas/git/git.test.ts b/test/smoke/src/areas/git/git.test.ts index cc383b9b8c5..498bfd7780e 100644 --- a/test/smoke/src/areas/git/git.test.ts +++ b/test/smoke/src/areas/git/git.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as cp from 'child_process'; -import { SpectronApplication } from '../../spectron/application'; +import { Application } from '../../application'; const DIFF_EDITOR_LINE_INSERT = '.monaco-diff-editor .editor.modified .line-insert'; const SYNC_STATUSBAR = 'div[id="workbench.parts.statusbar"] .statusbar-entry a[title$="Synchronize Changes"]'; @@ -12,39 +12,40 @@ const SYNC_STATUSBAR = 'div[id="workbench.parts.statusbar"] .statusbar-entry a[t export function setup() { describe('Git', () => { before(async function () { - const app = this.app as SpectronApplication; - app.suiteName = 'Git'; + const app = this.app as Application; + + cp.execSync('git config user.name testuser', { cwd: app.workspacePath }); + cp.execSync('git config user.email monacotools@microsoft.com', { cwd: app.workspacePath }); }); it('reflects working tree changes', async function () { - const app = this.app as SpectronApplication; + const app = this.app as Application; await app.workbench.scm.openSCMViewlet(); await app.workbench.quickopen.openFile('app.js'); await app.workbench.editor.waitForTypeInEditor('app.js', '.foo{}'); - await app.workbench.saveOpenedFile(); + await app.workbench.editors.saveOpenedFile(); await app.workbench.quickopen.openFile('index.jade'); await app.workbench.editor.waitForTypeInEditor('index.jade', 'hello world'); - await app.workbench.saveOpenedFile(); + await app.workbench.editors.saveOpenedFile(); await app.workbench.scm.refreshSCMViewlet(); await app.workbench.scm.waitForChange('app.js', 'Modified'); await app.workbench.scm.waitForChange('index.jade', 'Modified'); - await app.screenCapturer.capture('changes'); }); it('opens diff editor', async function () { - const app = this.app as SpectronApplication; + const app = this.app as Application; await app.workbench.scm.openSCMViewlet(); await app.workbench.scm.openChange('app.js'); - await app.client.waitForElement(DIFF_EDITOR_LINE_INSERT); + await app.code.waitForElement(DIFF_EDITOR_LINE_INSERT); }); it('stages correctly', async function () { - const app = this.app as SpectronApplication; + const app = this.app as Application; await app.workbench.scm.openSCMViewlet(); @@ -58,7 +59,7 @@ export function setup() { }); it(`stages, commits changes and verifies outgoing change`, async function () { - const app = this.app as SpectronApplication; + const app = this.app as Application; await app.workbench.scm.openSCMViewlet(); @@ -67,13 +68,13 @@ export function setup() { await app.workbench.scm.waitForChange('app.js', 'Index Modified'); await app.workbench.scm.commit('first commit'); - await app.client.waitForText(SYNC_STATUSBAR, ' 0↓ 1↑'); + await app.code.waitForTextContent(SYNC_STATUSBAR, ' 0↓ 1↑'); - await app.workbench.quickopen.runCommand('Git: Stage All Changes'); + await app.workbench.runCommand('Git: Stage All Changes'); await app.workbench.scm.waitForChange('index.jade', 'Index Modified'); await app.workbench.scm.commit('second commit'); - await app.client.waitForText(SYNC_STATUSBAR, ' 0↓ 2↑'); + await app.code.waitForTextContent(SYNC_STATUSBAR, ' 0↓ 2↑'); cp.execSync('git reset --hard origin/master', { cwd: app.workspacePath }); }); diff --git a/test/smoke/src/areas/git/scm.ts b/test/smoke/src/areas/git/scm.ts index 7f25c3a3881..9c2a6f3e871 100644 --- a/test/smoke/src/areas/git/scm.ts +++ b/test/smoke/src/areas/git/scm.ts @@ -3,8 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { SpectronApplication } from '../../spectron/application'; import { Viewlet } from '../workbench/viewlet'; +import { Commands } from '../workbench/workbench'; +import { IElement } from '../../vscode/driver'; +import { findElement, findElements, Code } from '../../vscode/code'; const VIEWLET = 'div[id="workbench.view.scm"]'; const SCM_INPUT = `${VIEWLET} .scm-editor textarea`; @@ -22,84 +24,61 @@ interface Change { actions: string[]; } +function toChange(element: IElement): Change { + const name = findElement(element, e => /\blabel-name\b/.test(e.className))!; + const type = element.attributes['data-tooltip'] || ''; + + const actionElementList = findElements(element, e => /\baction-label\b/.test(e.className)); + const actions = actionElementList.map(e => e.attributes['title']); + + return { + name: name.textContent || '', + type, + actions + }; +} + + export class SCM extends Viewlet { - constructor(spectron: SpectronApplication) { - super(spectron); + constructor(code: Code, private commands: Commands) { + super(code); } async openSCMViewlet(): Promise { - await this.spectron.runCommand('workbench.view.scm'); - await this.spectron.client.waitForElement(SCM_INPUT); + await this.commands.runCommand('workbench.view.scm'); + await this.code.waitForElement(SCM_INPUT); } - waitForChange(name: string, type?: string): Promise { - return this.spectron.client.waitFor(async () => { - const changes = await this.queryChanges(name, type); - return changes.length; - }, l => l > 0, 'Getting SCM changes') as Promise as Promise; + async waitForChange(name: string, type?: string): Promise { + const func = (change: Change) => change.name === name && (!type || change.type === type); + await this.code.waitForElements(SCM_RESOURCE, true, elements => elements.some(e => func(toChange(e)))); } async refreshSCMViewlet(): Promise { - await this.spectron.client.click(REFRESH_COMMAND); - } - - private async queryChanges(name: string, type?: string): Promise { - const result = await this.spectron.webclient.selectorExecute(SCM_RESOURCE, (div, name, type) => { - return (Array.isArray(div) ? div : [div]) - .map(element => { - const name = element.querySelector('.label-name') as HTMLElement; - const type = element.getAttribute('data-tooltip') || ''; - const actionElementList = element.querySelectorAll('.actions .action-label'); - const actions: string[] = []; - - for (let i = 0; i < actionElementList.length; i++) { - const element = actionElementList.item(i) as HTMLElement; - actions.push(element.title); - } - - return { - name: name.textContent, - type, - actions - }; - }) - .filter(change => { - if (change.name !== name) { - return false; - } - - if (type && (change.type !== type)) { - return false; - } - - return true; - }); - }, name, type); - - return result; + await this.code.waitAndClick(REFRESH_COMMAND); } async openChange(name: string): Promise { - await this.spectron.client.waitAndClick(SCM_RESOURCE_CLICK(name)); + await this.code.waitAndClick(SCM_RESOURCE_CLICK(name)); } async stage(name: string): Promise { - await this.spectron.client.waitAndClick(SCM_RESOURCE_ACTION_CLICK(name, 'Stage Changes')); + await this.code.waitAndClick(SCM_RESOURCE_ACTION_CLICK(name, 'Stage Changes')); } async stageAll(): Promise { - await this.spectron.client.waitAndClick(SCM_RESOURCE_GROUP_COMMAND_CLICK('Stage All Changes')); + await this.code.waitAndClick(SCM_RESOURCE_GROUP_COMMAND_CLICK('Stage All Changes')); } async unstage(name: string): Promise { - await this.spectron.client.waitAndClick(SCM_RESOURCE_ACTION_CLICK(name, 'Unstage Changes')); + await this.code.waitAndClick(SCM_RESOURCE_ACTION_CLICK(name, 'Unstage Changes')); } async commit(message: string): Promise { - await this.spectron.client.waitAndClick(SCM_INPUT); - await this.spectron.client.waitForActiveElement(SCM_INPUT); - await this.spectron.client.setValue(SCM_INPUT, message); - await this.spectron.client.waitAndClick(COMMIT_COMMAND); + await this.code.waitAndClick(SCM_INPUT); + await this.code.waitForActiveElement(SCM_INPUT); + await this.code.waitForSetValue(SCM_INPUT, message); + await this.code.waitAndClick(COMMIT_COMMAND); } } \ No newline at end of file diff --git a/test/smoke/src/areas/multiroot/multiroot.test.ts b/test/smoke/src/areas/multiroot/multiroot.test.ts index b81df706cf3..86449f54858 100644 --- a/test/smoke/src/areas/multiroot/multiroot.test.ts +++ b/test/smoke/src/areas/multiroot/multiroot.test.ts @@ -3,16 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; -import { SpectronApplication } from '../../spectron/application'; +import { Application } from '../../application'; export function setup() { describe('Multiroot', () => { before(async function () { - this.app.suiteName = 'Multiroot'; - - const app = this.app as SpectronApplication; + const app = this.app as Application; // restart with preventing additional windows from restoring // to ensure the window after restart is the multi-root workspace @@ -20,7 +17,7 @@ export function setup() { }); it('shows results from all folders', async function () { - const app = this.app as SpectronApplication; + const app = this.app as Application; await app.workbench.quickopen.openQuickOpen('*.*'); await app.workbench.quickopen.waitForQuickOpenElements(names => names.length === 6); @@ -28,10 +25,8 @@ export function setup() { }); it('shows workspace name in title', async function () { - const app = this.app as SpectronApplication; - const title = await app.client.getTitle(); - await app.screenCapturer.capture('window title'); - assert.ok(title.indexOf('smoketest (Workspace)') >= 0); + const app = this.app as Application; + await app.code.waitForTitle(title => /smoketest \(Workspace\)/i.test(title)); }); }); } \ No newline at end of file diff --git a/test/smoke/src/areas/preferences/keybindings.ts b/test/smoke/src/areas/preferences/keybindings.ts index 1de2eba3470..807a971891e 100644 --- a/test/smoke/src/areas/preferences/keybindings.ts +++ b/test/smoke/src/areas/preferences/keybindings.ts @@ -3,26 +3,28 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { SpectronApplication } from '../../spectron/application'; +import { Commands } from '../workbench/workbench'; +import { Code } from '../../vscode/code'; const SEARCH_INPUT = '.settings-search-input input'; export class KeybindingsEditor { - constructor(private spectron: SpectronApplication) { } + constructor(private code: Code, private commands: Commands) { } - async updateKeybinding(command: string, keys: string[], ariaLabel: string): Promise { - await this.spectron.runCommand('workbench.action.openGlobalKeybindings'); - await this.spectron.client.waitForActiveElement(SEARCH_INPUT); - await this.spectron.client.setValue(SEARCH_INPUT, command); + async updateKeybinding(command: string, keybinding: string, ariaLabel: string): Promise { + await this.commands.runCommand('workbench.action.openGlobalKeybindings'); + await this.code.waitForActiveElement(SEARCH_INPUT); + await this.code.waitForSetValue(SEARCH_INPUT, command); - await this.spectron.client.waitAndClick('div[aria-label="Keybindings"] .monaco-list-row.keybinding-item'); - await this.spectron.client.waitForElement('div[aria-label="Keybindings"] .monaco-list-row.keybinding-item.focused.selected'); + await this.code.waitAndClick('div[aria-label="Keybindings"] .monaco-list-row.keybinding-item'); + await this.code.waitForElement('div[aria-label="Keybindings"] .monaco-list-row.keybinding-item.focused.selected'); - await this.spectron.client.waitAndClick('div[aria-label="Keybindings"] .monaco-list-row.keybinding-item .action-item .icon.add'); - await this.spectron.client.waitForElement('.defineKeybindingWidget .monaco-inputbox.synthetic-focus'); + await this.code.waitAndClick('div[aria-label="Keybindings"] .monaco-list-row.keybinding-item .action-item .icon.add'); + await this.code.waitForElement('.defineKeybindingWidget .monaco-inputbox.synthetic-focus'); - await this.spectron.client.keys([...keys, 'NULL', 'Enter', 'NULL']); - await this.spectron.client.waitForElement(`div[aria-label="Keybindings"] div[aria-label="Keybinding is ${ariaLabel}."]`); + await this.code.dispatchKeybinding(keybinding); + await this.code.dispatchKeybinding('enter'); + await this.code.waitForElement(`div[aria-label="Keybindings"] div[aria-label="Keybinding is ${ariaLabel}."]`); } } \ No newline at end of file diff --git a/test/smoke/src/areas/preferences/preferences.test.ts b/test/smoke/src/areas/preferences/preferences.test.ts index f273d6a9c12..1b2bbbec548 100644 --- a/test/smoke/src/areas/preferences/preferences.test.ts +++ b/test/smoke/src/areas/preferences/preferences.test.ts @@ -3,45 +3,34 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; - -import { SpectronApplication } from '../../spectron/application'; +import { Application } from '../../application'; import { ActivityBarPosition } from '../activitybar/activityBar'; export function setup() { describe('Preferences', () => { - before(function () { - this.app.suiteName = 'Preferences'; - }); - it('turns off editor line numbers and verifies the live change', async function () { - const app = this.app as SpectronApplication; + const app = this.app as Application; await app.workbench.explorer.openFile('app.js'); - let lineNumbers = await app.client.waitForElements('.line-numbers'); - await app.screenCapturer.capture('app.js has line numbers'); - assert.ok(!!lineNumbers.length, 'Line numbers are not present in the editor before disabling them.'); + await app.code.waitForElements('.line-numbers', false, elements => !!elements.length); await app.workbench.settingsEditor.addUserSetting('editor.lineNumbers', '"off"'); - await app.workbench.selectTab('app.js'); - lineNumbers = await app.client.waitForElements('.line-numbers', result => !result || result.length === 0); - - await app.screenCapturer.capture('line numbers hidden'); - assert.ok(!lineNumbers.length, 'Line numbers are still present in the editor after disabling them.'); + await app.workbench.editors.selectTab('app.js'); + await app.code.waitForElements('.line-numbers', false, result => !result || result.length === 0); }); it(`changes 'workbench.action.toggleSidebarPosition' command key binding and verifies it`, async function () { - const app = this.app as SpectronApplication; - assert.ok(await app.workbench.activitybar.getActivityBar(ActivityBarPosition.LEFT), 'Activity bar should be positioned on the left.'); + const app = this.app as Application; + await app.workbench.activitybar.waitForActivityBar(ActivityBarPosition.LEFT); - await app.workbench.keybindingsEditor.updateKeybinding('workbench.action.toggleSidebarPosition', ['Control', 'u'], 'Control+U'); + await app.workbench.keybindingsEditor.updateKeybinding('workbench.action.toggleSidebarPosition', 'ctrl+u', 'Control+U'); - await app.client.keys(['Control', 'u', 'NULL']); - assert.ok(await app.workbench.activitybar.getActivityBar(ActivityBarPosition.RIGHT), 'Activity bar was not moved to right after toggling its position.'); + await app.code.dispatchKeybinding('ctrl+u'); + await app.workbench.activitybar.waitForActivityBar(ActivityBarPosition.RIGHT); }); after(async function () { - const app = this.app as SpectronApplication; + const app = this.app as Application; await app.workbench.settingsEditor.clearUserSettings(); }); }); diff --git a/test/smoke/src/areas/preferences/settings.ts b/test/smoke/src/areas/preferences/settings.ts index 657d71357e9..7d398598f30 100644 --- a/test/smoke/src/areas/preferences/settings.ts +++ b/test/smoke/src/areas/preferences/settings.ts @@ -5,7 +5,10 @@ import * as fs from 'fs'; import * as path from 'path'; -import { SpectronApplication } from '../../spectron/application'; +import { Commands } from '../workbench/workbench'; +import { Editor } from '../editor/editor'; +import { Editors } from '../editor/editors'; +import { Code } from '../../vscode/code'; export enum ActivityBarPosition { LEFT = 0, @@ -13,33 +16,28 @@ export enum ActivityBarPosition { } const SEARCH_INPUT = '.settings-search-input input'; -const EDITOR = '.editable-preferences-editor-container .monaco-editor textarea'; export class SettingsEditor { - constructor(private spectron: SpectronApplication) { } + constructor(private code: Code, private userDataPath: string, private commands: Commands, private editors: Editors, private editor: Editor) { } async addUserSetting(setting: string, value: string): Promise { - await this.spectron.runCommand('workbench.action.openGlobalSettings'); - await this.spectron.client.waitAndClick(SEARCH_INPUT); - await this.spectron.client.waitForActiveElement(SEARCH_INPUT); + await this.commands.runCommand('workbench.action.openGlobalSettings'); + await this.code.waitAndClick(SEARCH_INPUT); + await this.code.waitForActiveElement(SEARCH_INPUT); - await this.spectron.client.keys(['ArrowDown', 'NULL']); - await this.spectron.client.waitForActiveElement(EDITOR); + await this.editor.waitForEditorFocus('settings.json', 1, '.editable-preferences-editor-container'); - await this.spectron.client.keys(['ArrowRight', 'NULL']); - await this.spectron.screenCapturer.capture('user settings is open and focused'); - - await this.spectron.workbench.editor.waitForTypeInEditor('settings.json', `"${setting}": ${value}`, '.editable-preferences-editor-container'); - await this.spectron.workbench.saveOpenedFile(); - - await this.spectron.screenCapturer.capture('user settings has changed'); + await this.code.dispatchKeybinding('right'); + await this.editor.waitForTypeInEditor('settings.json', `"${setting}": ${value}`, '.editable-preferences-editor-container'); + await this.editors.saveOpenedFile(); } async clearUserSettings(): Promise { - const settingsPath = path.join(this.spectron.userDataPath, 'User', 'settings.json'); + const settingsPath = path.join(this.userDataPath, 'User', 'settings.json'); await new Promise((c, e) => fs.writeFile(settingsPath, '{}', 'utf8', err => err ? e(err) : c())); - await this.spectron.workbench.editor.waitForEditorContents('settings.json', c => c.length === 0, '.editable-preferences-editor-container'); + await this.commands.runCommand('workbench.action.openGlobalSettings'); + await this.editor.waitForEditorContents('settings.json', c => c === '{}', '.editable-preferences-editor-container'); } } \ No newline at end of file diff --git a/test/smoke/src/areas/problems/problems.ts b/test/smoke/src/areas/problems/problems.ts index 64156eced8e..0b82fa6914e 100644 --- a/test/smoke/src/areas/problems/problems.ts +++ b/test/smoke/src/areas/problems/problems.ts @@ -3,7 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { SpectronApplication } from '../../spectron/application'; +import { Commands } from '../workbench/workbench'; +import { Code } from '../../vscode/code'; export enum ProblemSeverity { WARNING = 0, @@ -14,31 +15,20 @@ export class Problems { static PROBLEMS_VIEW_SELECTOR = '.panel.markers-panel'; - constructor(private spectron: SpectronApplication) { - // noop - } + constructor(private code: Code, private commands: Commands) { } public async showProblemsView(): Promise { - if (!await this.isVisible()) { - await this.spectron.runCommand('workbench.actions.view.problems'); - await this.waitForProblemsView(); - } + await this.commands.runCommand('workbench.actions.view.problems'); + await this.waitForProblemsView(); } public async hideProblemsView(): Promise { - if (await this.isVisible()) { - await this.spectron.runCommand('workbench.actions.view.problems'); - await this.spectron.client.waitForElement(Problems.PROBLEMS_VIEW_SELECTOR, el => !el); - } - } - - public async isVisible(): Promise { - const element = await this.spectron.client.element(Problems.PROBLEMS_VIEW_SELECTOR); - return !!element; + await this.commands.runCommand('workbench.actions.view.problems'); + await this.code.waitForElement(Problems.PROBLEMS_VIEW_SELECTOR, el => !el); } public async waitForProblemsView(): Promise { - await this.spectron.client.waitForElement(Problems.PROBLEMS_VIEW_SELECTOR); + await this.code.waitForElement(Problems.PROBLEMS_VIEW_SELECTOR); } public static getSelectorInProblemsView(problemType: ProblemSeverity): string { @@ -47,7 +37,7 @@ export class Problems { } public static getSelectorInEditor(problemType: ProblemSeverity): string { - let selector = problemType === ProblemSeverity.WARNING ? 'squiggly-c-warning' : 'squiggly-d-error'; + let selector = problemType === ProblemSeverity.WARNING ? 'squiggly-warning' : 'squiggly-error'; return `.view-overlays .cdr.${selector}`; } } diff --git a/test/smoke/src/areas/quickopen/quickopen.ts b/test/smoke/src/areas/quickopen/quickopen.ts index 9d76495c81f..70b60ec7ab7 100644 --- a/test/smoke/src/areas/quickopen/quickopen.ts +++ b/test/smoke/src/areas/quickopen/quickopen.ts @@ -3,7 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { SpectronApplication } from '../../spectron/application'; +import { Editors } from '../editor/editors'; +import { Commands } from '../workbench/workbench'; +import { Code } from '../../vscode/code'; export class QuickOpen { @@ -12,77 +14,99 @@ export class QuickOpen { static QUICK_OPEN_INPUT = `${QuickOpen.QUICK_OPEN} .quick-open-input input`; static QUICK_OPEN_FOCUSED_ELEMENT = `${QuickOpen.QUICK_OPEN} .quick-open-tree .monaco-tree-row.focused .monaco-highlighted-label`; static QUICK_OPEN_ENTRY_SELECTOR = 'div[aria-label="Quick Picker"] .monaco-tree-rows.show-twisties .monaco-tree-row .quick-open-entry'; + static QUICK_OPEN_ENTRY_LABEL_SELECTOR = 'div[aria-label="Quick Picker"] .monaco-tree-rows.show-twisties .monaco-tree-row .quick-open-entry .label-name'; - constructor(readonly spectron: SpectronApplication) { } + constructor(private code: Code, private commands: Commands, private editors: Editors) { } async openQuickOpen(value: string): Promise { - await this.spectron.runCommand('workbench.action.quickOpen'); - await this.waitForQuickOpenOpened(); + let retries = 0; + + // other parts of code might steal focus away from quickopen :( + while (retries < 5) { + await this.commands.runCommand('workbench.action.quickOpen'); + + try { + await this.waitForQuickOpenOpened(10); + break; + } catch (err) { + if (++retries > 5) { + throw err; + } + + await this.code.dispatchKeybinding('escape'); + } + } if (value) { - await this.spectron.client.setValue(QuickOpen.QUICK_OPEN_INPUT, value); + await this.code.waitForSetValue(QuickOpen.QUICK_OPEN_INPUT, value); } } async closeQuickOpen(): Promise { - await this.spectron.runCommand('workbench.action.closeQuickOpen'); + await this.commands.runCommand('workbench.action.closeQuickOpen'); await this.waitForQuickOpenClosed(); } async openFile(fileName: string): Promise { await this.openQuickOpen(fileName); - await this.waitForQuickOpenElements(names => names.some(n => n === fileName)); - await this.spectron.client.keys(['Enter', 'NULL']); - await this.spectron.workbench.waitForActiveTab(fileName); - await this.spectron.workbench.waitForEditorFocus(fileName); + await this.waitForQuickOpenElements(names => names[0] === fileName); + await this.code.dispatchKeybinding('enter'); + await this.editors.waitForActiveTab(fileName); + await this.editors.waitForEditorFocus(fileName); } - async runCommand(commandText: string): Promise { - await this.openQuickOpen(`> ${commandText}`); - - // wait for best choice to be focused - await this.spectron.client.waitForTextContent(QuickOpen.QUICK_OPEN_FOCUSED_ELEMENT, commandText); - - // wait and click on best choice - await this.spectron.client.waitAndClick(QuickOpen.QUICK_OPEN_FOCUSED_ELEMENT); - } - - async waitForQuickOpenOpened(): Promise { - await this.spectron.client.waitForActiveElement(QuickOpen.QUICK_OPEN_INPUT); + async waitForQuickOpenOpened(retryCount?: number): Promise { + await this.code.waitForActiveElement(QuickOpen.QUICK_OPEN_INPUT, retryCount); } private async waitForQuickOpenClosed(): Promise { - await this.spectron.client.waitForElement(QuickOpen.QUICK_OPEN_HIDDEN); + await this.code.waitForElement(QuickOpen.QUICK_OPEN_HIDDEN); } async submit(text: string): Promise { - await this.spectron.client.setValue(QuickOpen.QUICK_OPEN_INPUT, text); - await this.spectron.client.keys(['Enter', 'NULL']); + await this.code.waitForSetValue(QuickOpen.QUICK_OPEN_INPUT, text); + await this.code.dispatchKeybinding('enter'); await this.waitForQuickOpenClosed(); } async selectQuickOpenElement(index: number): Promise { await this.waitForQuickOpenOpened(); for (let from = 0; from < index; from++) { - await this.spectron.client.keys(['ArrowDown', 'NULL']); + await this.code.dispatchKeybinding('down'); } - await this.spectron.client.keys(['Enter', 'NULL']); + await this.code.dispatchKeybinding('enter'); await this.waitForQuickOpenClosed(); } async waitForQuickOpenElements(accept: (names: string[]) => boolean): Promise { - await this.spectron.client.waitFor(() => this.getQuickOpenElements(), accept); + await this.code.waitForElements(QuickOpen.QUICK_OPEN_ENTRY_LABEL_SELECTOR, false, els => accept(els.map(e => e.textContent))); } - private async getQuickOpenElements(): Promise { - const result = await this.spectron.webclient.selectorExecute(QuickOpen.QUICK_OPEN_ENTRY_SELECTOR, - div => (Array.isArray(div) ? div : [div]).map(element => { - const name = element.querySelector('.label-name') as HTMLElement; - return name.textContent; - }) - ); + async runCommand(command: string): Promise { + await this.openQuickOpen(`> ${command}`); - return Array.isArray(result) ? result : []; + // wait for best choice to be focused + await this.code.waitForTextContent(QuickOpen.QUICK_OPEN_FOCUSED_ELEMENT, command); + + // wait and click on best choice + await this.code.waitAndClick(QuickOpen.QUICK_OPEN_FOCUSED_ELEMENT); + } + + async openQuickOutline(): Promise { + let retries = 0; + + while (++retries < 10) { + await this.commands.runCommand('workbench.action.gotoSymbol'); + + const text = await this.code.waitForTextContent('div[aria-label="Quick Picker"] .monaco-tree-rows.show-twisties div.monaco-tree-row .quick-open-entry .monaco-icon-label .label-name .monaco-highlighted-label span'); + + if (text !== 'No symbol information for the file') { + return; + } + + await this.closeQuickOpen(); + await new Promise(c => setTimeout(c, 250)); + } } } diff --git a/test/smoke/src/areas/search/search.test.ts b/test/smoke/src/areas/search/search.test.ts index a0e932298b6..484e428e7b1 100644 --- a/test/smoke/src/areas/search/search.test.ts +++ b/test/smoke/src/areas/search/search.test.ts @@ -3,16 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { SpectronApplication } from '../../spectron/application'; +import { Application } from '../../application'; export function setup() { describe('Search', () => { - before(function () { - this.app.suiteName = 'Search'; - }); - it('searches for body & checks for correct result number', async function () { - const app = this.app as SpectronApplication; + const app = this.app as Application; await app.workbench.search.openSearchViewlet(); await app.workbench.search.searchFor('body'); @@ -20,7 +16,7 @@ export function setup() { }); it('searches only for *.js files & checks for correct result number', async function () { - const app = this.app as SpectronApplication; + const app = this.app as Application; await app.workbench.search.searchFor('body'); await app.workbench.search.showQueryDetails(); await app.workbench.search.setFilesToIncludeText('*.js'); @@ -32,27 +28,25 @@ export function setup() { }); it('dismisses result & checks for correct result number', async function () { - const app = this.app as SpectronApplication; + const app = this.app as Application; await app.workbench.search.searchFor('body'); await app.workbench.search.removeFileMatch(1); await app.workbench.search.waitForResultText('10 results in 4 files'); }); it('replaces first search result with a replace term', async function () { - const app = this.app as SpectronApplication; + const app = this.app as Application; await app.workbench.search.searchFor('body'); await app.workbench.search.expandReplace(); await app.workbench.search.setReplaceText('ydob'); await app.workbench.search.replaceFileMatch(1); - await app.workbench.saveOpenedFile(); await app.workbench.search.waitForResultText('10 results in 4 files'); await app.workbench.search.searchFor('ydob'); await app.workbench.search.setReplaceText('body'); await app.workbench.search.replaceFileMatch(1); - await app.workbench.saveOpenedFile(); }); }); } \ No newline at end of file diff --git a/test/smoke/src/areas/search/search.ts b/test/smoke/src/areas/search/search.ts index f2d98866b02..9052aaca126 100644 --- a/test/smoke/src/areas/search/search.ts +++ b/test/smoke/src/areas/search/search.ts @@ -3,85 +3,77 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { SpectronApplication } from '../../spectron/application'; import { Viewlet } from '../workbench/viewlet'; +import { Commands } from '../workbench/workbench'; +import { Code } from '../../vscode/code'; const VIEWLET = 'div[id="workbench.view.search"] .search-view'; const INPUT = `${VIEWLET} .search-widget .search-container .monaco-inputbox input`; -const INCLUDE_INPUT = `${VIEWLET} .query-details .monaco-inputbox input[aria-label="Search Include Patterns"]`; +const INCLUDE_INPUT = `${VIEWLET} .query-details .file-types.includes .monaco-inputbox input`; export class Search extends Viewlet { - constructor(spectron: SpectronApplication) { - super(spectron); + constructor(code: Code, private commands: Commands) { + super(code); } async openSearchViewlet(): Promise { - await this.spectron.runCommand('workbench.view.search'); - await this.spectron.client.waitForActiveElement(INPUT); + await this.commands.runCommand('workbench.view.search'); + await this.code.waitForActiveElement(INPUT); } async searchFor(text: string): Promise { - await this.spectron.client.click(INPUT); - await this.spectron.client.waitForActiveElement(INPUT); - await this.spectron.client.setValue(INPUT, text); + await this.code.waitAndClick(INPUT); + await this.code.waitForActiveElement(INPUT); + await this.code.waitForSetValue(INPUT, text); await this.submitSearch(); } async submitSearch(): Promise { - await this.spectron.client.click(INPUT); - await this.spectron.client.waitForActiveElement(INPUT); + await this.code.waitAndClick(INPUT); + await this.code.waitForActiveElement(INPUT); - await this.spectron.client.keys(['Enter', 'NULL']); - await this.spectron.client.element(`${VIEWLET} .messages[aria-hidden="false"]`); + await this.code.dispatchKeybinding('enter'); + await this.code.waitForElement(`${VIEWLET} .messages[aria-hidden="false"]`); } async setFilesToIncludeText(text: string): Promise { - await this.spectron.client.click(INCLUDE_INPUT); - await this.spectron.client.waitForActiveElement(INCLUDE_INPUT); - await this.spectron.client.setValue(INCLUDE_INPUT, text || ''); + await this.code.waitAndClick(INCLUDE_INPUT); + await this.code.waitForActiveElement(INCLUDE_INPUT); + await this.code.waitForSetValue(INCLUDE_INPUT, text || ''); } async showQueryDetails(): Promise { - if (!await this.areDetailsVisible()) { - await this.spectron.client.waitAndClick(`${VIEWLET} .query-details .more`); - } + await this.code.waitAndClick(`${VIEWLET} .query-details .more`); } async hideQueryDetails(): Promise { - if (await this.areDetailsVisible()) { - await this.spectron.client.waitAndClick(`${VIEWLET} .query-details.more .more`); - } - } - - async areDetailsVisible(): Promise { - const element = await this.spectron.client.element(`${VIEWLET} .query-details.more`); - return !!element; + await this.code.waitAndClick(`${VIEWLET} .query-details.more .more`); } async removeFileMatch(index: number): Promise { - await this.spectron.client.waitAndMoveToObject(`${VIEWLET} .results .monaco-tree-rows>:nth-child(${index}) .filematch`); - const file = await this.spectron.client.waitForText(`${VIEWLET} .results .monaco-tree-rows>:nth-child(${index}) .filematch a.label-name`); - await this.spectron.client.waitAndClick(`${VIEWLET} .results .monaco-tree-rows>:nth-child(${index}) .filematch .action-label.icon.action-remove`); - await this.spectron.client.waitForText(`${VIEWLET} .results .monaco-tree-rows>:nth-child(${index}) .filematch a.label-name`, void 0, result => result !== file); + await this.code.waitAndMove(`${VIEWLET} .results .monaco-tree-rows>:nth-child(${index}) .filematch`); + const file = await this.code.waitForTextContent(`${VIEWLET} .results .monaco-tree-rows>:nth-child(${index}) .filematch a.label-name`); + await this.code.waitAndClick(`${VIEWLET} .results .monaco-tree-rows>:nth-child(${index}) .filematch .action-label.icon.action-remove`); + await this.code.waitForTextContent(`${VIEWLET} .results .monaco-tree-rows>:nth-child(${index}) .filematch a.label-name`, void 0, result => result !== file); } async expandReplace(): Promise { - await this.spectron.client.waitAndClick(`${VIEWLET} .search-widget .monaco-button.toggle-replace-button.collapse`); + await this.code.waitAndClick(`${VIEWLET} .search-widget .monaco-button.toggle-replace-button.collapse`); } async setReplaceText(text: string): Promise { - await this.spectron.client.waitAndClick(`${VIEWLET} .search-widget .replace-container .monaco-inputbox input[title="Replace"]`); - await this.spectron.client.element(`${VIEWLET} .search-widget .replace-container .monaco-inputbox.synthetic-focus input[title="Replace"]`); - await this.spectron.client.setValue(`${VIEWLET} .search-widget .replace-container .monaco-inputbox.synthetic-focus input[title="Replace"]`, text); + await this.code.waitAndClick(`${VIEWLET} .search-widget .replace-container .monaco-inputbox input[title="Replace"]`); + await this.code.waitForElement(`${VIEWLET} .search-widget .replace-container .monaco-inputbox.synthetic-focus input[title="Replace"]`); + await this.code.waitForSetValue(`${VIEWLET} .search-widget .replace-container .monaco-inputbox.synthetic-focus input[title="Replace"]`, text); } async replaceFileMatch(index: number): Promise { - await this.spectron.client.waitAndMoveToObject(`${VIEWLET} .results .monaco-tree-rows>:nth-child(${index}) .filematch`); - await this.spectron.client.click(`${VIEWLET} .results .monaco-tree-rows>:nth-child(${index}) .filematch .action-label.icon.action-replace-all`); + await this.code.waitAndMove(`${VIEWLET} .results .monaco-tree-rows>:nth-child(${index}) .filematch`); + await this.code.waitAndClick(`${VIEWLET} .results .monaco-tree-rows>:nth-child(${index}) .filematch .action-label.icon.action-replace-all`); } async waitForResultText(text: string): Promise { - await this.spectron.client.waitForText(`${VIEWLET} .messages[aria-hidden="false"] .message>p`, text); + await this.code.waitForTextContent(`${VIEWLET} .messages[aria-hidden="false"] .message>p`, text); } } diff --git a/test/smoke/src/areas/statusbar/statusbar.test.ts b/test/smoke/src/areas/statusbar/statusbar.test.ts index 044af05c8d9..9fcd47bf9b5 100644 --- a/test/smoke/src/areas/statusbar/statusbar.test.ts +++ b/test/smoke/src/areas/statusbar/statusbar.test.ts @@ -3,19 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; - -import { SpectronApplication, Quality } from '../../spectron/application'; +import { Application, Quality } from '../../application'; import { StatusBarElement } from './statusbar'; export function setup() { describe('Statusbar', () => { - before(function () { - this.app.suiteName = 'Statusbar'; - }); - it('verifies presence of all default status bar elements', async function () { - const app = this.app as SpectronApplication; + const app = this.app as Application; await app.workbench.statusbar.waitForStatusbarElement(StatusBarElement.BRANCH_STATUS); if (app.quality !== Quality.Dev) { @@ -33,7 +27,7 @@ export function setup() { }); it(`verifies that 'quick open' opens when clicking on status bar elements`, async function () { - const app = this.app as SpectronApplication; + const app = this.app as Application; await app.workbench.statusbar.clickOn(StatusBarElement.BRANCH_STATUS); await app.workbench.quickopen.waitForQuickOpenOpened(); @@ -55,25 +49,25 @@ export function setup() { }); it(`verifies that 'Problems View' appears when clicking on 'Problems' status element`, async function () { - const app = this.app as SpectronApplication; + const app = this.app as Application; await app.workbench.statusbar.clickOn(StatusBarElement.PROBLEMS_STATUS); await app.workbench.problems.waitForProblemsView(); }); it(`verifies that 'Tweet us feedback' pop-up appears when clicking on 'Feedback' icon`, async function () { - const app = this.app as SpectronApplication; + const app = this.app as Application; if (app.quality === Quality.Dev) { return this.skip(); } await app.workbench.statusbar.clickOn(StatusBarElement.FEEDBACK_ICON); - assert.ok(!!await app.client.waitForElement('.feedback-form')); + await app.code.waitForElement('.feedback-form'); }); it(`checks if 'Go to Line' works if called from the status bar`, async function () { - const app = this.app as SpectronApplication; + const app = this.app as Application; await app.workbench.quickopen.openFile('app.js'); await app.workbench.statusbar.clickOn(StatusBarElement.SELECTION_STATUS); @@ -81,11 +75,11 @@ export function setup() { await app.workbench.quickopen.waitForQuickOpenOpened(); await app.workbench.quickopen.submit(':15'); - await app.workbench.editor.waitForHighlightingLine(15); + await app.workbench.editor.waitForHighlightingLine('app.js', 15); }); it(`verifies if changing EOL is reflected in the status bar`, async function () { - const app = this.app as SpectronApplication; + const app = this.app as Application; await app.workbench.quickopen.openFile('app.js'); await app.workbench.statusbar.clickOn(StatusBarElement.EOL_STATUS); diff --git a/test/smoke/src/areas/statusbar/statusbar.ts b/test/smoke/src/areas/statusbar/statusbar.ts index b33121994d6..dd3392a5a9c 100644 --- a/test/smoke/src/areas/statusbar/statusbar.ts +++ b/test/smoke/src/areas/statusbar/statusbar.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { SpectronApplication } from '../../spectron/application'; +import { Code } from '../../vscode/code'; export enum StatusBarElement { BRANCH_STATUS = 0, @@ -23,23 +23,22 @@ export class StatusBar { private readonly leftSelector = '.statusbar-item.left'; private readonly rightSelector = '.statusbar-item.right'; - constructor(private spectron: SpectronApplication) { + constructor(private code: Code) { } + + async waitForStatusbarElement(element: StatusBarElement): Promise { + await this.code.waitForElement(this.getSelector(element)); } - public async waitForStatusbarElement(element: StatusBarElement): Promise { - await this.spectron.client.waitForElement(this.getSelector(element)); + async clickOn(element: StatusBarElement): Promise { + await this.code.waitAndClick(this.getSelector(element)); } - public async clickOn(element: StatusBarElement): Promise { - await this.spectron.client.waitAndClick(this.getSelector(element)); + async waitForEOL(eol: string): Promise { + return this.code.waitForTextContent(this.getSelector(StatusBarElement.EOL_STATUS), eol); } - public async waitForEOL(eol: string): Promise { - return this.spectron.client.waitForText(this.getSelector(StatusBarElement.EOL_STATUS), eol); - } - - public async getStatusbarTextByTitle(title: string): Promise { - return await this.spectron.client.waitForText(`${this.mainSelector} span[title="smoke test"]`); + async waitForStatusbarText(title: string, text: string): Promise { + await this.code.waitForTextContent(`${this.mainSelector} span[title="${title}"]`, text); } private getSelector(element: StatusBarElement): string { diff --git a/test/smoke/src/areas/terminal/terminal.test.ts b/test/smoke/src/areas/terminal/terminal.test.ts index abbb77e61d5..4c8c27e3794 100644 --- a/test/smoke/src/areas/terminal/terminal.test.ts +++ b/test/smoke/src/areas/terminal/terminal.test.ts @@ -3,27 +3,24 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -// import { SpectronApplication } from '../../spectron/application'; +import { Application } from '../../application'; -describe('Terminal', () => { - // let app: SpectronApplication; - // before(() => { app = new SpectronApplication(); return app.start('Terminal'); }); - // after(() => app.stop()); +export function setup() { + describe('Terminal', () => { + it(`opens terminal, runs 'echo' and verifies the output`, async function () { + const app = this.app as Application; - // it(`opens terminal, runs 'echo' and verifies the output`, async function () { - // const expected = new Date().getTime().toString(); - // await app.workbench.terminal.showTerminal(); - - // await app.workbench.terminal.runCommand(`echo ${expected}`); - - // await app.workbench.terminal.waitForTerminalText(terminalText => { - // // Last line will not contain the output - // for (let index = terminalText.length - 2; index >= 0; index--) { - // if (!!terminalText[index] && terminalText[index].trim() === expected) { - // return true; - // } - // } - // return false; - // }); - // }); -}); \ No newline at end of file + const expected = new Date().getTime().toString(); + await app.workbench.terminal.showTerminal(); + await app.workbench.terminal.runCommand(`echo ${expected}`); + await app.workbench.terminal.waitForTerminalText(terminalText => { + for (let index = terminalText.length - 2; index >= 0; index--) { + if (!!terminalText[index] && terminalText[index].trim() === expected) { + return true; + } + } + return false; + }); + }); + }); +} \ No newline at end of file diff --git a/test/smoke/src/areas/terminal/terminal.ts b/test/smoke/src/areas/terminal/terminal.ts index 951497edccf..6e5e02735ca 100644 --- a/test/smoke/src/areas/terminal/terminal.ts +++ b/test/smoke/src/areas/terminal/terminal.ts @@ -3,59 +3,29 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { SpectronApplication } from '../../spectron/application'; +import { Code } from '../../vscode/code'; +import { Commands } from '../workbench/workbench'; const PANEL_SELECTOR = 'div[id="workbench.panel.terminal"]'; const XTERM_SELECTOR = `${PANEL_SELECTOR} .terminal-wrapper`; +const XTERM_TEXTAREA = `${XTERM_SELECTOR} textarea.xterm-helper-textarea`; export class Terminal { - constructor(private spectron: SpectronApplication) { } + constructor(private code: Code, private commands: Commands) { } async showTerminal(): Promise { - if (!await this.isVisible()) { - await this.spectron.workbench.quickopen.runCommand('View: Toggle Integrated Terminal'); - await this.spectron.client.waitForElement(XTERM_SELECTOR); - await this.waitForTerminalText(text => text.length > 0, 'Waiting for Terminal to be ready'); - } - } - - async isVisible(): Promise { - const element = await this.spectron.client.element(PANEL_SELECTOR); - return !!element; + await this.commands.runCommand('workbench.action.terminal.toggleTerminal'); + await this.code.waitForActiveElement(XTERM_TEXTAREA); + await this.code.waitForTerminalBuffer(XTERM_SELECTOR, lines => lines.some(line => line.length > 0)); } async runCommand(commandText: string): Promise { - // TODO@Tyriar fix this. we should not use type but setValue - // await this.spectron.client.type(commandText); - await this.spectron.client.keys(['Enter', 'NULL']); + await this.code.waitForPaste(XTERM_TEXTAREA, commandText); + await this.code.dispatchKeybinding('enter'); } - async waitForTerminalText(fn: (text: string[]) => boolean, timeOutDescription: string = 'Getting Terminal Text'): Promise { - return this.spectron.client.waitFor(async () => { - const terminalText = await this.getTerminalText(); - if (fn(terminalText)) { - return terminalText; - } - return undefined; - }, void 0, timeOutDescription); - } - - getCurrentLineNumber(): Promise { - return this.getTerminalText().then(text => text.length); - } - - private async getTerminalText(): Promise { - return await this.spectron.webclient.selectorExecute(XTERM_SELECTOR, - div => { - const xterm = ((Array.isArray(div) ? div[0] : div)).xterm; - const buffer = xterm.buffer; - const lines: string[] = []; - for (let i = 0; i < buffer.lines.length; i++) { - lines.push(buffer.translateBufferLineToString(i, true)); - } - return lines; - } - ); + async waitForTerminalText(accept: (buffer: string[]) => boolean): Promise { + await this.code.waitForTerminalBuffer(XTERM_SELECTOR, accept); } } \ No newline at end of file diff --git a/test/smoke/src/areas/workbench/data-loss.test.ts b/test/smoke/src/areas/workbench/data-loss.test.ts index 8e6cb42d4ab..4dc1e37ab26 100644 --- a/test/smoke/src/areas/workbench/data-loss.test.ts +++ b/test/smoke/src/areas/workbench/data-loss.test.ts @@ -3,39 +3,30 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { SpectronApplication } from '../../spectron/application'; +import { Application } from '../../application'; export function setup() { describe('Dataloss', () => { - before(function () { - this.app.suiteName = 'Dataloss'; - }); - it(`verifies that 'hot exit' works for dirty files`, async function () { - const app = this.app as SpectronApplication; - await app.workbench.newUntitledFile(); + const app = this.app as Application; + await app.workbench.editors.newUntitledFile(); const untitled = 'Untitled-1'; const textToTypeInUntitled = 'Hello, Unitled Code'; await app.workbench.editor.waitForTypeInEditor(untitled, textToTypeInUntitled); - await app.screenCapturer.capture('Untitled file before reload'); const readmeMd = 'readme.md'; const textToType = 'Hello, Code'; await app.workbench.explorer.openFile(readmeMd); await app.workbench.editor.waitForTypeInEditor(readmeMd, textToType); - await app.screenCapturer.capture(`${readmeMd} before reload`); await app.reload(); - await app.screenCapturer.capture('After reload'); - await app.workbench.waitForActiveTab(readmeMd, true); - await app.screenCapturer.capture(`${readmeMd} after reload`); + await app.workbench.editors.waitForActiveTab(readmeMd, true); await app.workbench.editor.waitForEditorContents(readmeMd, c => c.indexOf(textToType) > -1); - await app.workbench.waitForTab(untitled, true); - await app.workbench.selectTab(untitled, true); - await app.screenCapturer.capture('Untitled file after reload'); + await app.workbench.editors.waitForTab(untitled, true); + await app.workbench.editors.selectTab(untitled, true); await app.workbench.editor.waitForEditorContents(untitled, c => c.indexOf(textToTypeInUntitled) > -1); }); }); diff --git a/test/smoke/src/areas/workbench/data-migration.test.ts b/test/smoke/src/areas/workbench/data-migration.test.ts index 7a2877ab4ad..f4084ca624b 100644 --- a/test/smoke/src/areas/workbench/data-migration.test.ts +++ b/test/smoke/src/areas/workbench/data-migration.test.ts @@ -3,130 +3,109 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; - -import { SpectronApplication, Quality } from '../../spectron/application'; +import { Application, Quality } from '../../application'; import * as rimraf from 'rimraf'; export interface ICreateAppFn { - (quality: Quality): SpectronApplication | null; + (quality: Quality): Application; } export function setup(userDataDir: string, createApp: ICreateAppFn) { describe('Data Migration', () => { + afterEach(async function () { await new Promise((c, e) => rimraf(userDataDir, { maxBusyTries: 10 }, err => err ? e(err) : c())); }); - it('checks if the Untitled file is restored migrating from stable to latest', async function () { - const stableApp = createApp(Quality.Stable); + // it('checks if the Untitled file is restored migrating from stable to latest', async function () { + // const stableApp = createApp(Quality.Stable); - if (!stableApp) { - this.skip(); - return; - } + // if (!stableApp) { + // this.skip(); + // return; + // } - await stableApp.start(); - stableApp.suiteName = 'Data Migration'; + // await stableApp.start(); - const textToType = 'Very dirty file'; + // const textToType = 'Very dirty file'; - await stableApp.workbench.newUntitledFile(); - await stableApp.workbench.editor.waitForTypeInEditor('Untitled-1', textToType); + // await stableApp.workbench.editors.newUntitledFile(); + // await stableApp.workbench.editor.waitForTypeInEditor('Untitled-1', textToType); - await stableApp.stop(); - await new Promise(c => setTimeout(c, 500)); // wait until all resources are released (e.g. locked local storage) + // await stableApp.stop(); + // await new Promise(c => setTimeout(c, 500)); // wait until all resources are released (e.g. locked local storage) - // Checking latest version for the restored state - const app = createApp(Quality.Insiders); + // // Checking latest version for the restored state + // const app = createApp(Quality.Insiders); - if (!app) { - return assert(false); - } + // await app.start(false); - await app.start(false); - app.suiteName = 'Data Migration'; + // await app.workbench.editors.waitForActiveTab('Untitled-1', true); + // await app.workbench.editor.waitForEditorContents('Untitled-1', c => c.indexOf(textToType) > -1); - assert.ok(await app.workbench.waitForActiveTab('Untitled-1', true), `Untitled-1 tab is not present after migration.`); + // await app.stop(); + // }); - await app.workbench.editor.waitForEditorContents('Untitled-1', c => c.indexOf(textToType) > -1); - await app.screenCapturer.capture('Untitled file text'); + // it('checks if the newly created dirty file is restored migrating from stable to latest', async function () { + // const stableApp = createApp(Quality.Stable); - await app.stop(); - }); + // if (!stableApp) { + // this.skip(); + // return; + // } - it('checks if the newly created dirty file is restored migrating from stable to latest', async function () { - const stableApp = createApp(Quality.Stable); + // await stableApp.start(); - if (!stableApp) { - this.skip(); - return; - } + // const fileName = 'app.js'; + // const textPart = 'This is going to be an unsaved file'; - await stableApp.start(); - stableApp.suiteName = 'Data Migration'; + // await stableApp.workbench.quickopen.openFile(fileName); - const fileName = 'app.js'; - const textPart = 'This is going to be an unsaved file'; + // await stableApp.workbench.editor.waitForTypeInEditor(fileName, textPart); - await stableApp.workbench.quickopen.openFile(fileName); + // await stableApp.stop(); + // await new Promise(c => setTimeout(c, 500)); // wait until all resources are released (e.g. locked local storage) - await stableApp.workbench.editor.waitForTypeInEditor(fileName, textPart); + // // Checking latest version for the restored state + // const app = createApp(Quality.Insiders); - await stableApp.stop(); - await new Promise(c => setTimeout(c, 500)); // wait until all resources are released (e.g. locked local storage) + // await app.start(false); - // Checking latest version for the restored state - const app = createApp(Quality.Insiders); + // await app.workbench.editors.waitForActiveTab(fileName); + // await app.workbench.editor.waitForEditorContents(fileName, c => c.indexOf(textPart) > -1); - if (!app) { - return assert(false); - } + // await app.stop(); + // }); - await app.start(false); - app.suiteName = 'Data Migration'; + // it('checks if opened tabs are restored migrating from stable to latest', async function () { + // const stableApp = createApp(Quality.Stable); - assert.ok(await app.workbench.waitForActiveTab(fileName), `dirty file tab is not present after migration.`); - await app.workbench.editor.waitForEditorContents(fileName, c => c.indexOf(textPart) > -1); + // if (!stableApp) { + // this.skip(); + // return; + // } - await app.stop(); - }); + // await stableApp.start(); - it('checks if opened tabs are restored migrating from stable to latest', async function () { - const stableApp = createApp(Quality.Stable); + // const fileName1 = 'app.js', fileName2 = 'jsconfig.json', fileName3 = 'readme.md'; - if (!stableApp) { - this.skip(); - return; - } + // await stableApp.workbench.quickopen.openFile(fileName1); + // await stableApp.workbench.runCommand('View: Keep Editor'); + // await stableApp.workbench.quickopen.openFile(fileName2); + // await stableApp.workbench.runCommand('View: Keep Editor'); + // await stableApp.workbench.quickopen.openFile(fileName3); + // await stableApp.stop(); - await stableApp.start(); - stableApp.suiteName = 'Data Migration'; + // const app = createApp(Quality.Insiders); - const fileName1 = 'app.js', fileName2 = 'jsconfig.json', fileName3 = 'readme.md'; + // await app.start(false); - await stableApp.workbench.quickopen.openFile(fileName1); - await stableApp.workbench.quickopen.runCommand('View: Keep Editor'); - await stableApp.workbench.quickopen.openFile(fileName2); - await stableApp.workbench.quickopen.runCommand('View: Keep Editor'); - await stableApp.workbench.quickopen.openFile(fileName3); - await stableApp.stop(); + // await app.workbench.editors.waitForTab(fileName1); + // await app.workbench.editors.waitForTab(fileName2); + // await app.workbench.editors.waitForTab(fileName3); - const app = createApp(Quality.Insiders); - - if (!app) { - return assert(false); - } - - await app.start(false); - app.suiteName = 'Data Migration'; - - assert.ok(await app.workbench.waitForTab(fileName1), `${fileName1} tab was not restored after migration.`); - assert.ok(await app.workbench.waitForTab(fileName2), `${fileName2} tab was not restored after migration.`); - assert.ok(await app.workbench.waitForTab(fileName3), `${fileName3} tab was not restored after migration.`); - - await app.stop(); - }); + // await app.stop(); + // }); }); } \ No newline at end of file diff --git a/test/smoke/src/areas/workbench/localization.test.ts b/test/smoke/src/areas/workbench/localization.test.ts index 0251a9bf964..ea7a25bf8cf 100644 --- a/test/smoke/src/areas/workbench/localization.test.ts +++ b/test/smoke/src/areas/workbench/localization.test.ts @@ -3,15 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; - -import { SpectronApplication, Quality } from '../../spectron/application'; +import { Application, Quality } from '../../application'; export function setup() { describe('Localization', () => { before(async function () { - const app = this.app as SpectronApplication; - this.app.suiteName = 'Localization'; + const app = this.app as Application; if (app.quality === Quality.Dev) { return; @@ -21,36 +18,26 @@ export function setup() { }); it(`starts with 'DE' locale and verifies title and viewlets text is in German`, async function () { - const app = this.app as SpectronApplication; + const app = this.app as Application; if (app.quality === Quality.Dev) { this.skip(); return; } - let text = await app.workbench.explorer.getOpenEditorsViewTitle(); - await app.screenCapturer.capture('Open editors title'); - assert(/geöffnete editoren/i.test(text)); + await app.workbench.explorer.waitForOpenEditorsViewTitle(title => /geöffnete editoren/i.test(title)); await app.workbench.search.openSearchViewlet(); - text = await app.workbench.search.getTitle(); - await app.screenCapturer.capture('Search title'); - assert(/suchen/i.test(text)); + await app.workbench.search.waitForTitle(title => /suchen/i.test(title)); await app.workbench.scm.openSCMViewlet(); - text = await app.workbench.scm.getTitle(); - await app.screenCapturer.capture('Scm title'); - assert(/quellcodeverwaltung/i.test(text)); + await app.workbench.scm.waitForTitle(title => /quellcodeverwaltung/i.test(title)); await app.workbench.debug.openDebugViewlet(); - text = await app.workbench.debug.getTitle(); - await app.screenCapturer.capture('Debug title'); - assert(/debuggen/i.test(text)); + await app.workbench.debug.waitForTitle(title => /debuggen/i.test(title)); await app.workbench.extensions.openExtensionsViewlet(); - text = await app.workbench.extensions.getTitle(); - await app.screenCapturer.capture('Extensions title'); - assert(/erweiterungen/i.test(text)); + await app.workbench.extensions.waitForTitle(title => /erweiterungen/i.test(title)); }); }); } \ No newline at end of file diff --git a/test/smoke/src/areas/workbench/viewlet.ts b/test/smoke/src/areas/workbench/viewlet.ts index 7eb114edaaf..2293752b705 100644 --- a/test/smoke/src/areas/workbench/viewlet.ts +++ b/test/smoke/src/areas/workbench/viewlet.ts @@ -3,16 +3,15 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { SpectronApplication } from '../../spectron/application'; +'use strict'; + +import { Code } from '../../vscode/code'; export abstract class Viewlet { - constructor(protected spectron: SpectronApplication) { - // noop - } + constructor(protected code: Code) { } - public async getTitle(): Promise { - return this.spectron.client.waitForText('.monaco-workbench-container .part.sidebar > .title > .title-label > span'); + async waitForTitle(fn: (title: string) => boolean): Promise { + await this.code.waitForTextContent('.monaco-workbench-container .part.sidebar > .title > .title-label > span', undefined, fn); } - } \ No newline at end of file diff --git a/test/smoke/src/areas/workbench/workbench.ts b/test/smoke/src/areas/workbench/workbench.ts index c2711f449d3..5ea8b5a7f9a 100644 --- a/test/smoke/src/areas/workbench/workbench.ts +++ b/test/smoke/src/areas/workbench/workbench.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { SpectronApplication } from '../../spectron/application'; import { Explorer } from '../explorer/explorer'; import { ActivityBar } from '../activitybar/activityBar'; import { QuickOpen } from '../quickopen/quickopen'; @@ -16,13 +15,20 @@ import { StatusBar } from '../statusbar/statusbar'; import { Problems } from '../problems/problems'; import { SettingsEditor } from '../preferences/settings'; import { KeybindingsEditor } from '../preferences/keybindings'; +import { Editors } from '../editor/editors'; +import { Code } from '../../vscode/code'; import { Terminal } from '../terminal/terminal'; -export class Workbench { +export interface Commands { + runCommand(command: string): Promise; +} +export class Workbench implements Commands { + + readonly quickopen: QuickOpen; + readonly editors: Editors; readonly explorer: Explorer; readonly activitybar: ActivityBar; - readonly quickopen: QuickOpen; readonly search: Search; readonly extensions: Extensions; readonly editor: Editor; @@ -34,47 +40,35 @@ export class Workbench { readonly keybindingsEditor: KeybindingsEditor; readonly terminal: Terminal; - constructor(private spectron: SpectronApplication) { - this.explorer = new Explorer(spectron); - this.activitybar = new ActivityBar(spectron); - this.quickopen = new QuickOpen(spectron); - this.search = new Search(spectron); - this.extensions = new Extensions(spectron); - this.editor = new Editor(spectron); - this.scm = new SCM(spectron); - this.debug = new Debug(spectron); - this.statusbar = new StatusBar(spectron); - this.problems = new Problems(spectron); - this.settingsEditor = new SettingsEditor(spectron); - this.keybindingsEditor = new KeybindingsEditor(spectron); - this.terminal = new Terminal(spectron); + constructor(private code: Code, private keybindings: any[], userDataPath: string) { + this.editors = new Editors(code, this); + this.quickopen = new QuickOpen(code, this, this.editors); + this.explorer = new Explorer(code, this.quickopen, this.editors); + this.activitybar = new ActivityBar(code); + this.search = new Search(code, this); + this.extensions = new Extensions(code, this); + this.editor = new Editor(code, this); + this.scm = new SCM(code, this); + this.debug = new Debug(code, this, this.editors, this.editor); + this.statusbar = new StatusBar(code); + this.problems = new Problems(code, this); + this.settingsEditor = new SettingsEditor(code, userDataPath, this, this.editors, this.editor); + this.keybindingsEditor = new KeybindingsEditor(code, this); + this.terminal = new Terminal(code, this); } - public async saveOpenedFile(): Promise { - await this.spectron.client.waitForElement('.tabs-container div.tab.active.dirty'); - await this.spectron.workbench.quickopen.runCommand('File: Save'); - } + /** + * Retrieves the command from keybindings file and executes it with WebdriverIO client API + * @param command command (e.g. 'workbench.action.files.newUntitledFile') + */ + async runCommand(command: string): Promise { + const binding = this.keybindings.find(x => x['command'] === command); - public async selectTab(tabName: string, untitled: boolean = false): Promise { - await this.spectron.client.waitAndClick(`.tabs-container div.tab[aria-label="${tabName}, tab"]`); - await this.waitForEditorFocus(tabName, untitled); - } - - public async waitForEditorFocus(fileName: string, untitled: boolean = false): Promise { - await this.waitForActiveTab(fileName); - await this.editor.waitForActiveEditor(fileName); - } - - public async waitForActiveTab(fileName: string, isDirty: boolean = false): Promise { - return this.spectron.client.waitForElement(`.tabs-container div.tab.active${isDirty ? '.dirty' : ''}[aria-selected="true"][aria-label="${fileName}, tab"]`); - } - - public async waitForTab(fileName: string, isDirty: boolean = false): Promise { - return this.spectron.client.waitForElement(`.tabs-container div.tab${isDirty ? '.dirty' : ''}[aria-label="${fileName}, tab"]`).then(() => true); - } - - public async newUntitledFile(): Promise { - await this.spectron.runCommand('workbench.action.files.newUntitledFile'); - await this.waitForEditorFocus('Untitled-1', true); + if (binding) { + await this.code.dispatchKeybinding(binding.key); + } else { + await this.quickopen.runCommand(command); + } } } + diff --git a/test/smoke/src/helpers/screenshot.ts b/test/smoke/src/helpers/screenshot.ts deleted file mode 100644 index 18a068ec388..00000000000 --- a/test/smoke/src/helpers/screenshot.ts +++ /dev/null @@ -1,37 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as path from 'path'; -import * as fs from 'fs'; -import * as mkdirp from 'mkdirp'; -import { Application } from 'spectron'; -import { sanitize } from './utilities'; - -export class ScreenCapturer { - - private static counter = 0; - - constructor( - private application: Application, - public suiteName: string, - private screenshotsDirPath: string | undefined, - ) { } - - async capture(name: string): Promise { - if (!this.screenshotsDirPath) { - return; - } - - const screenshotPath = path.join( - this.screenshotsDirPath, - sanitize(this.suiteName), - `${ScreenCapturer.counter++}-${sanitize(name)}.png` - ); - - const image = await this.application.browserWindow.capturePage(); - await new Promise((c, e) => mkdirp(path.dirname(screenshotPath), err => err ? e(err) : c())); - await new Promise((c, e) => fs.writeFile(screenshotPath, image, err => err ? e(err) : c())); - } -} diff --git a/test/smoke/src/helpers/utilities.ts b/test/smoke/src/helpers/utilities.ts deleted file mode 100644 index 8b86d31c957..00000000000 --- a/test/smoke/src/helpers/utilities.ts +++ /dev/null @@ -1,53 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as fs from 'fs'; -import { dirname } from 'path'; - -export function nfcall(fn: Function, ...args): Promise { - return new Promise((c, e) => fn(...args, (err, r) => err ? e(err) : c(r))); -} - -export async function mkdirp(path: string, mode?: number): Promise { - const mkdir = async () => { - try { - await nfcall(fs.mkdir, path, mode); - } catch (err) { - if (err.code === 'EEXIST') { - const stat = await nfcall(fs.stat, path); - - if (stat.isDirectory) { - return; - } - - throw new Error(`'${path}' exists and is not a directory.`); - } - - throw err; - } - }; - - // is root? - if (path === dirname(path)) { - return true; - } - - try { - await mkdir(); - } catch (err) { - if (err.code !== 'ENOENT') { - throw err; - } - - await mkdirp(dirname(path), mode); - await mkdir(); - } - - return true; -} - -export function sanitize(name: string): string { - return name.replace(/[&*:\/]/g, ''); -} \ No newline at end of file diff --git a/test/smoke/src/logger.ts b/test/smoke/src/logger.ts new file mode 100644 index 00000000000..b36b502d6ae --- /dev/null +++ b/test/smoke/src/logger.ts @@ -0,0 +1,42 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { appendFileSync, writeFileSync } from 'fs'; +import { format } from 'util'; +import { EOL } from 'os'; + +export interface Logger { + log(message: string, ...args: any[]): void; +} + +export class ConsoleLogger implements Logger { + + log(message: string, ...args: any[]): void { + console.log('**', message, ...args); + } +} + +export class FileLogger implements Logger { + + constructor(private path: string) { + writeFileSync(path, ''); + } + + log(message: string, ...args: any[]): void { + const date = new Date().toISOString(); + appendFileSync(this.path, `[${date}] ${format(message, ...args)}${EOL}`); + } +} + +export class MultiLogger implements Logger { + + constructor(private loggers: Logger[]) { } + + log(message: string, ...args: any[]): void { + for (const logger of this.loggers) { + logger.log(message, ...args); + } + } +} \ No newline at end of file diff --git a/test/smoke/src/main.ts b/test/smoke/src/main.ts index 8adeab24a78..eb74ad61c79 100644 --- a/test/smoke/src/main.ts +++ b/test/smoke/src/main.ts @@ -11,9 +11,9 @@ import * as minimist from 'minimist'; import * as tmp from 'tmp'; import * as rimraf from 'rimraf'; import * as mkdirp from 'mkdirp'; -import { SpectronApplication, Quality } from './spectron/application'; -import { setup as setupDataMigrationTests } from './areas/workbench/data-migration.test'; +import { Application, Quality } from './application'; +import { setup as setupDataMigrationTests } from './areas/workbench/data-migration.test'; import { setup as setupDataLossTests } from './areas/workbench/data-loss.test'; import { setup as setupDataExplorerTests } from './areas/explorer/explorer.test'; import { setup as setupDataPreferencesTests } from './areas/preferences/preferences.test'; @@ -24,9 +24,10 @@ import { setup as setupDataDebugTests } from './areas/debug/debug.test'; import { setup as setupDataGitTests } from './areas/git/git.test'; import { setup as setupDataStatusbarTests } from './areas/statusbar/statusbar.test'; import { setup as setupDataExtensionTests } from './areas/extensions/extensions.test'; +import { setup as setupTerminalTests } from './areas/terminal/terminal.test'; import { setup as setupDataMultirootTests } from './areas/multiroot/multiroot.test'; import { setup as setupDataLocalizationTests } from './areas/workbench/localization.test'; -// import './areas/terminal/terminal.test'; +import { MultiLogger, Logger, ConsoleLogger, FileLogger } from './logger'; const tmpDir = tmp.dirSync({ prefix: 't' }) as { name: string; removeCallback: Function; }; const testDataPath = tmpDir.name; @@ -37,13 +38,20 @@ const opts = minimist(args, { string: [ 'build', 'stable-build', - 'log', - 'wait-time' - ] + 'wait-time', + 'test-repo', + 'keybindings', + 'screenshots', + 'log' + ], + boolean: [ + 'verbose' + ], + default: { + verbose: false + } }); -const artifactsPath = opts.log || ''; - const workspaceFilePath = path.join(testDataPath, 'smoketest.code-workspace'); const testRepoUrl = 'https://github.com/Microsoft/vscode-smoketest-express'; const workspacePath = path.join(testDataPath, 'vscode-smoketest-express'); @@ -51,6 +59,12 @@ const keybindingsPath = path.join(testDataPath, 'keybindings.json'); const extensionsPath = path.join(testDataPath, 'extensions-dir'); mkdirp.sync(extensionsPath); +const screenshotsPath = opts.screenshots ? path.resolve(opts.screenshots) : null; + +if (screenshotsPath) { + mkdirp.sync(screenshotsPath); +} + function fail(errorMessage): void { console.error(errorMessage); process.exit(1); @@ -96,16 +110,16 @@ function getBuildElectronPath(root: string): string { } let testCodePath = opts.build; -let stableCodePath = opts['stable-build']; +// let stableCodePath = opts['stable-build']; let electronPath: string; -let stablePath: string; +// let stablePath: string; if (testCodePath) { electronPath = getBuildElectronPath(testCodePath); - if (stableCodePath) { - stablePath = getBuildElectronPath(stableCodePath); - } + // if (stableCodePath) { + // stablePath = getBuildElectronPath(stableCodePath); + // } } else { testCodePath = getDevElectronPath(); electronPath = testCodePath; @@ -147,96 +161,106 @@ function toUri(path: string): string { return `${path}`; } +async function getKeybindings(): Promise { + if (opts.keybindings) { + console.log('*** Using keybindings: ', opts.keybindings); + const rawKeybindings = fs.readFileSync(opts.keybindings); + fs.writeFileSync(keybindingsPath, rawKeybindings); + } else { + const keybindingsUrl = `https://raw.githubusercontent.com/Microsoft/vscode-docs/master/build/keybindings/doc.keybindings.${getKeybindingPlatform()}.json`; + console.log('*** Fetching keybindings...'); + + await new Promise((c, e) => { + https.get(keybindingsUrl, res => { + const output = fs.createWriteStream(keybindingsPath); + res.on('error', e); + output.on('error', e); + output.on('close', c); + res.pipe(output); + }).on('error', e); + }); + } +} + +async function createWorkspaceFile(): Promise { + if (fs.existsSync(workspaceFilePath)) { + return; + } + + console.log('*** Creating workspace file...'); + const workspace = { + folders: [ + { + path: toUri(path.join(workspacePath, 'public')) + }, + { + path: toUri(path.join(workspacePath, 'routes')) + }, + { + path: toUri(path.join(workspacePath, 'views')) + } + ] + }; + + fs.writeFileSync(workspaceFilePath, JSON.stringify(workspace, null, '\t')); +} + +async function setupRepository(): Promise { + if (opts['test-repo']) { + console.log('*** Copying test project repository:', opts['test-repo']); + rimraf.sync(workspacePath); + // not platform friendly + cp.execSync(`cp -R "${opts['test-repo']}" "${workspacePath}"`); + } else { + if (!fs.existsSync(workspacePath)) { + console.log('*** Cloning test project repository...'); + cp.spawnSync('git', ['clone', testRepoUrl, workspacePath]); + } else { + console.log('*** Cleaning test project repository...'); + cp.spawnSync('git', ['fetch'], { cwd: workspacePath }); + cp.spawnSync('git', ['reset', '--hard', 'FETCH_HEAD'], { cwd: workspacePath }); + cp.spawnSync('git', ['clean', '-xdf'], { cwd: workspacePath }); + } + + console.log('*** Running npm install...'); + cp.execSync('npm install', { cwd: workspacePath, stdio: 'inherit' }); + } +} + async function setup(): Promise { console.log('*** Test data:', testDataPath); console.log('*** Preparing smoketest setup...'); - const keybindingsUrl = `https://raw.githubusercontent.com/Microsoft/vscode-docs/master/build/keybindings/doc.keybindings.${getKeybindingPlatform()}.json`; - console.log('*** Fetching keybindings...'); - - await new Promise((c, e) => { - https.get(keybindingsUrl, res => { - const output = fs.createWriteStream(keybindingsPath); - res.on('error', e); - output.on('error', e); - output.on('close', c); - res.pipe(output); - }).on('error', e); - }); - - if (!fs.existsSync(workspaceFilePath)) { - console.log('*** Creating workspace file...'); - const workspace = { - folders: [ - { - path: toUri(path.join(workspacePath, 'public')) - }, - { - path: toUri(path.join(workspacePath, 'routes')) - }, - { - path: toUri(path.join(workspacePath, 'views')) - } - ] - }; - - fs.writeFileSync(workspaceFilePath, JSON.stringify(workspace, null, '\t')); - } - - if (!fs.existsSync(workspacePath)) { - console.log('*** Cloning test project repository...'); - cp.spawnSync('git', ['clone', testRepoUrl, workspacePath]); - } else { - console.log('*** Cleaning test project repository...'); - cp.spawnSync('git', ['fetch'], { cwd: workspacePath }); - cp.spawnSync('git', ['reset', '--hard', 'FETCH_HEAD'], { cwd: workspacePath }); - cp.spawnSync('git', ['clean', '-xdf'], { cwd: workspacePath }); - } - - console.log('*** Running npm install...'); - cp.execSync('npm install', { cwd: workspacePath, stdio: 'inherit' }); + await getKeybindings(); + await createWorkspaceFile(); + await setupRepository(); console.log('*** Smoketest setup done!\n'); } -/** - * WebDriverIO 4.8.0 outputs all kinds of "deprecation" warnings - * for common commands like `keys` and `moveToObject`. - * According to https://github.com/Codeception/CodeceptJS/issues/531, - * these deprecation warnings are for Firefox, and have no alternative replacements. - * Since we can't downgrade WDIO as suggested (it's Spectron's dep, not ours), - * we must suppress the warning with a classic monkey-patch. - * - * @see webdriverio/lib/helpers/depcrecationWarning.js - * @see https://github.com/webdriverio/webdriverio/issues/2076 - */ -// Filter out the following messages: -const wdioDeprecationWarning = /^WARNING: the "\w+" command will be deprecated soon../; // [sic] -// Monkey patch: -const warn = console.warn; -console.warn = function suppressWebdriverWarnings(message) { - if (wdioDeprecationWarning.test(message)) { return; } - warn.apply(console, arguments); -}; +function createApp(quality: Quality): Application { + const loggers: Logger[] = []; -function createApp(quality: Quality): SpectronApplication | null { - const path = quality === Quality.Stable ? stablePath : electronPath; - - if (!path) { - return null; + if (opts.verbose) { + loggers.push(new ConsoleLogger()); } - return new SpectronApplication({ + if (opts.log) { + loggers.push(new FileLogger(opts.log)); + } + + return new Application({ quality, - electronPath: path, + codePath: opts.build, workspacePath, userDataDir, extensionsPath, - artifactsPath, workspaceFilePath, - waitTime: parseInt(opts['wait-time'] || '0') || 20 + waitTime: parseInt(opts['wait-time'] || '0') || 20, + logger: new MultiLogger(loggers) }); } + before(async function () { // allow two minutes for setup this.timeout(2 * 60 * 1000); @@ -244,6 +268,7 @@ before(async function () { }); after(async function () { + await new Promise(c => setTimeout(c, 500)); // wait for shutdown await new Promise((c, e) => rimraf(testDataPath, { maxBusyTries: 10 }, err => err ? e(err) : c())); }); @@ -251,7 +276,7 @@ describe('Data Migration', () => { setupDataMigrationTests(userDataDir, createApp); }); -describe('Everything Else', () => { +describe('Test', () => { before(async function () { const app = createApp(quality); await app!.start(); @@ -262,6 +287,36 @@ describe('Everything Else', () => { await this.app.stop(); }); + if (screenshotsPath) { + afterEach(async function () { + if (this.currentTest.state !== 'failed') { + return; + } + + const app = this.app as Application; + const raw = await app.capturePage(); + const buffer = new Buffer(raw, 'base64'); + + const name = this.currentTest.fullTitle().replace(/[^a-z0-9\-]/ig, '_'); + const screenshotPath = path.join(screenshotsPath, `${name}.png`); + + if (opts.log) { + app.logger.log('*** Screenshot recorded:', screenshotPath); + } + + fs.writeFileSync(screenshotPath, buffer); + }); + } + + if (opts.log) { + beforeEach(async function () { + const app = this.app as Application; + const title = this.currentTest.fullTitle(); + + app.logger.log('*** Test start:', title); + }); + } + setupDataLossTests(); setupDataExplorerTests(); setupDataPreferencesTests(); @@ -272,6 +327,7 @@ describe('Everything Else', () => { setupDataGitTests(); setupDataStatusbarTests(); setupDataExtensionTests(); + setupTerminalTests(); setupDataMultirootTests(); setupDataLocalizationTests(); }); diff --git a/test/smoke/src/spectron/application.ts b/test/smoke/src/spectron/application.ts deleted file mode 100644 index 91d742cd159..00000000000 --- a/test/smoke/src/spectron/application.ts +++ /dev/null @@ -1,351 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Application, SpectronClient as WebClient } from 'spectron'; -import { test as testPort } from 'portastic'; -import { SpectronClient } from './client'; -import { ScreenCapturer } from '../helpers/screenshot'; -import { Workbench } from '../areas/workbench/workbench'; -import * as fs from 'fs'; -import * as cp from 'child_process'; -import * as path from 'path'; -import * as mkdirp from 'mkdirp'; -import { sanitize } from '../helpers/utilities'; - -// Just hope random helps us here, cross your fingers! -export async function findFreePort(): Promise { - for (let i = 0; i < 10; i++) { - const port = 10000 + Math.round(Math.random() * 10000); - - if (await testPort(port)) { - return port; - } - } - - throw new Error('Could not find free port!'); -} - -export enum Quality { - Dev, - Insiders, - Stable -} - -export interface SpectronApplicationOptions { - quality: Quality; - electronPath: string; - workspacePath: string; - userDataDir: string; - extensionsPath: string; - artifactsPath: string; - workspaceFilePath: string; - waitTime: number; -} - -/** - * Wraps Spectron's Application instance with its used methods. - */ -export class SpectronApplication { - - private static count = 0; - - private _client: SpectronClient; - private _workbench: Workbench; - private _screenCapturer: ScreenCapturer; - private spectron: Application | undefined; - private keybindings: any[]; private stopLogCollection: (() => Promise) | undefined; - - constructor( - private options: SpectronApplicationOptions - ) { } - - get quality(): Quality { - return this.options.quality; - } - - get client(): SpectronClient { - return this._client; - } - - get webclient(): WebClient { - if (!this.spectron) { - throw new Error('Application not started'); - } - - return this.spectron.client; - } - - get screenCapturer(): ScreenCapturer { - return this._screenCapturer; - } - - get workbench(): Workbench { - return this._workbench; - } - - get workspacePath(): string { - return this.options.workspacePath; - } - - get extensionsPath(): string { - return this.options.extensionsPath; - } - - get userDataPath(): string { - return this.options.userDataDir; - } - - get workspaceFilePath(): string { - return this.options.workspaceFilePath; - } - - private _suiteName: string = 'Init'; - - set suiteName(suiteName: string) { - this._suiteName = suiteName; - this._screenCapturer.suiteName = suiteName; - } - - async start(waitForWelcome: boolean = true): Promise { - await this._start(); - - if (waitForWelcome) { - await this.waitForWelcome(); - } - } - - async restart(options: { workspaceOrFolder?: string, extraArgs?: string[] }): Promise { - await this.stop(); - await new Promise(c => setTimeout(c, 1000)); - await this._start(options.workspaceOrFolder, options.extraArgs); - } - - private async _start(workspaceOrFolder = this.options.workspacePath, extraArgs: string[] = []): Promise { - await this.retrieveKeybindings(); - cp.execSync('git checkout .', { cwd: this.options.workspacePath }); - await this.startApplication(workspaceOrFolder, extraArgs); - await this.checkWindowReady(); - } - - async reload(): Promise { - await this.workbench.quickopen.runCommand('Reload Window'); - // TODO @sandy: Find a proper condition to wait for reload - await new Promise(c => setTimeout(c, 1500)); - await this.checkWindowReady(); - } - - async stop(): Promise { - if (this.stopLogCollection) { - await this.stopLogCollection(); - this.stopLogCollection = undefined; - } - - if (this.spectron && this.spectron.isRunning()) { - await this.spectron.stop(); - this.spectron = undefined; - } - } - - private async startApplication(workspaceOrFolder: string, extraArgs: string[] = []): Promise { - - let args: string[] = []; - let chromeDriverArgs: string[] = []; - - if (process.env.VSCODE_REPOSITORY) { - args.push(process.env.VSCODE_REPOSITORY as string); - } - - args.push(workspaceOrFolder); - - // Prevent 'Getting Started' web page from opening on clean user-data-dir - args.push('--skip-getting-started'); - - // Prevent 'Getting Started' web page from opening on clean user-data-dir - args.push('--skip-release-notes'); - - // Prevent Quick Open from closing when focus is stolen, this allows concurrent smoketest suite running - args.push('--sticky-quickopen'); - - // Disable telemetry - args.push('--disable-telemetry'); - - // Disable updates - args.push('--disable-updates'); - - // Disable crash reporter - // This seems to be the fix for the strange hangups in which Code stays unresponsive - // and tests finish badly with timeouts, leaving Code running in the background forever - args.push('--disable-crash-reporter'); - - // Ensure that running over custom extensions directory, rather than picking up the one that was used by a tester previously - args.push(`--extensions-dir=${this.options.extensionsPath}`); - - args.push(...extraArgs); - - chromeDriverArgs.push(`--user-data-dir=${this.options.userDataDir}`); - - // Spectron always uses the same port number for the chrome driver - // and it handles gracefully when two instances use the same port number - // This works, but when one of the instances quits, it takes down - // chrome driver with it, leaving the other instance in DISPAIR!!! :( - const port = await findFreePort(); - - // We must get a different port for debugging the smoketest express app - // otherwise concurrent test runs will clash on those ports - const env = { PORT: String(await findFreePort()), ...process.env }; - - const opts: any = { - path: this.options.electronPath, - port, - args, - env, - chromeDriverArgs, - startTimeout: 10000, - requireName: 'nodeRequire' - }; - - const runName = String(SpectronApplication.count++); - let testsuiteRootPath: string | undefined = undefined; - let screenshotsDirPath: string | undefined = undefined; - - if (this.options.artifactsPath) { - testsuiteRootPath = path.join(this.options.artifactsPath, sanitize(runName)); - mkdirp.sync(testsuiteRootPath); - - // Collect screenshots - screenshotsDirPath = path.join(testsuiteRootPath, 'screenshots'); - mkdirp.sync(screenshotsDirPath); - - // Collect chromedriver logs - const chromedriverLogPath = path.join(testsuiteRootPath, 'chromedriver.log'); - opts.chromeDriverLogPath = chromedriverLogPath; - - // Collect webdriver logs - const webdriverLogsPath = path.join(testsuiteRootPath, 'webdriver'); - mkdirp.sync(webdriverLogsPath); - opts.webdriverLogPath = webdriverLogsPath; - } - - this.spectron = new Application(opts); - await this.spectron.start(); - - if (testsuiteRootPath) { - // Collect logs - const mainProcessLogPath = path.join(testsuiteRootPath, 'main.log'); - const rendererProcessLogPath = path.join(testsuiteRootPath, 'renderer.log'); - - const flush = async () => { - if (!this.spectron) { - return; - } - - const mainLogs = await this.spectron.client.getMainProcessLogs(); - await new Promise((c, e) => fs.appendFile(mainProcessLogPath, mainLogs.join('\n'), { encoding: 'utf8' }, err => err ? e(err) : c())); - - const rendererLogs = (await this.spectron.client.getRenderProcessLogs()).map(m => `${m.timestamp} - ${m.level} - ${m.message}`); - await new Promise((c, e) => fs.appendFile(rendererProcessLogPath, rendererLogs.join('\n'), { encoding: 'utf8' }, err => err ? e(err) : c())); - }; - - let running = true; - const loopFlush = async () => { - while (true) { - await flush(); - - if (!running) { - return; - } - - await new Promise(c => setTimeout(c, 1000)); - } - }; - - const loopPromise = loopFlush(); - this.stopLogCollection = () => { - running = false; - return loopPromise; - }; - } - - this._screenCapturer = new ScreenCapturer(this.spectron, this._suiteName, screenshotsDirPath); - this._client = new SpectronClient(this.spectron, this, this.options.waitTime); - this._workbench = new Workbench(this); - } - - private async checkWindowReady(): Promise { - await this.webclient.waitUntilWindowLoaded(); - - // Pick the first workbench window here - const count = await this.webclient.getWindowCount(); - - for (let i = 0; i < count; i++) { - await this.webclient.windowByIndex(i); - - if (/bootstrap\/index\.html/.test(await this.webclient.getUrl())) { - break; - } - } - - await this.client.waitForElement('.monaco-workbench'); - } - - private async waitForWelcome(): Promise { - await this.client.waitForElement('.explorer-folders-view'); - await this.client.waitForElement(`.editor-container[id="workbench.editor.walkThroughPart"] .welcomePage`); - } - - private retrieveKeybindings(): Promise { - return new Promise((c, e) => { - fs.readFile(process.env.VSCODE_KEYBINDINGS_PATH as string, 'utf8', (err, data) => { - if (err) { - throw err; - } - try { - this.keybindings = JSON.parse(data); - c(); - } catch (e) { - throw new Error(`Error parsing keybindings JSON: ${e}`); - } - }); - }); - } - - /** - * Retrieves the command from keybindings file and executes it with WebdriverIO client API - * @param command command (e.g. 'workbench.action.files.newUntitledFile') - */ - runCommand(command: string): Promise { - const binding = this.keybindings.find(x => x['command'] === command); - if (!binding) { - return this.workbench.quickopen.runCommand(command); - } - - const keys: string = binding.key; - let keysToPress: string[] = []; - - const chords = keys.split(' '); - chords.forEach((chord) => { - const keys = chord.split('+'); - keys.forEach((key) => keysToPress.push(this.transliterate(key))); - keysToPress.push('NULL'); - }); - - return this.client.keys(keysToPress); - } - - /** - * Transliterates key names from keybindings file to WebdriverIO keyboard actions defined in: - * https://w3c.github.io/webdriver/webdriver-spec.html#keyboard-actions - */ - private transliterate(key: string): string { - switch (key) { - case 'ctrl': - return 'Control'; - case 'cmd': - return 'Meta'; - default: - return key.length === 1 ? key : key.charAt(0).toUpperCase() + key.slice(1); - } - } -} diff --git a/test/smoke/src/spectron/client.ts b/test/smoke/src/spectron/client.ts deleted file mode 100644 index 428d6a18310..00000000000 --- a/test/smoke/src/spectron/client.ts +++ /dev/null @@ -1,204 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Application } from 'spectron'; -import { RawResult, Element } from 'webdriverio'; -import { SpectronApplication } from './application'; - -/** - * Abstracts the Spectron's WebdriverIO managed client property on the created Application instances. - */ -export class SpectronClient { - - // waitFor calls should not take more than 200 * 100 = 20 seconds to complete, excluding - // the time it takes for the actual retry call to complete - private retryCount: number; - private readonly retryDuration = 100; // in milliseconds - - constructor( - readonly spectron: Application, - private application: SpectronApplication, - waitTime: number - ) { - this.retryCount = (waitTime * 1000) / this.retryDuration; - } - - keys(keys: string[]): Promise { - this.spectron.client.keys(keys); - return Promise.resolve(); - } - - async getText(selector: string, capture: boolean = true): Promise { - return this.spectron.client.getText(selector); - } - - async waitForText(selector: string, text?: string, accept?: (result: string) => boolean): Promise { - accept = accept ? accept : result => text !== void 0 ? text === result : !!result; - return this.waitFor(() => this.spectron.client.getText(selector), accept, `getText with selector ${selector}`); - } - - async waitForTextContent(selector: string, textContent?: string, accept?: (result: string) => boolean): Promise { - accept = accept ? accept : (result => textContent !== void 0 ? textContent === result : !!result); - const fn = async () => await this.spectron.client.selectorExecute(selector, div => Array.isArray(div) ? div[0].textContent : div.textContent); - return this.waitFor(fn, s => accept!(typeof s === 'string' ? s : ''), `getTextContent with selector ${selector}`); - } - - async waitForValue(selector: string, value?: string, accept?: (result: string) => boolean): Promise { - accept = accept ? accept : result => value !== void 0 ? value === result : !!result; - return this.waitFor(() => this.spectron.client.getValue(selector), accept, `getValue with selector ${selector}`); - } - - async waitAndClick(selector: string): Promise { - return this.waitFor(() => this.spectron.client.click(selector), void 0, `click with selector ${selector}`); - } - - async click(selector: string): Promise { - return this.spectron.client.click(selector); - } - - async doubleClickAndWait(selector: string, capture: boolean = true): Promise { - return this.waitFor(() => this.spectron.client.doubleClick(selector), void 0, `doubleClick with selector ${selector}`); - } - - async leftClick(selector: string, xoffset: number, yoffset: number, capture: boolean = true): Promise { - return this.spectron.client.leftClick(selector, xoffset, yoffset); - } - - async rightClick(selector: string, capture: boolean = true): Promise { - return this.spectron.client.rightClick(selector); - } - - async moveToObject(selector: string, capture: boolean = true): Promise { - return this.spectron.client.moveToObject(selector); - } - - async waitAndMoveToObject(selector: string): Promise { - return this.waitFor(() => this.spectron.client.moveToObject(selector), void 0, `move to object with selector ${selector}`); - } - - async setValue(selector: string, text: string, capture: boolean = true): Promise { - return this.spectron.client.setValue(selector, text); - } - - async waitForElements(selector: string, accept: (result: Element[]) => boolean = result => result.length > 0): Promise { - return this.waitFor>(() => this.spectron.client.elements(selector), result => accept(result.value), `elements with selector ${selector}`) - .then(result => result.value); - } - - async waitForElement(selector: string, accept: (result: Element | undefined) => boolean = result => !!result): Promise { - return this.waitFor>(() => this.spectron.client.element(selector), result => accept(result ? result.value : void 0), `element with selector ${selector}`) - .then(result => result.value); - } - - async waitForVisibility(selector: string, accept: (result: boolean) => boolean = result => result): Promise { - return this.waitFor(() => this.spectron.client.isVisible(selector), accept, `isVisible with selector ${selector}`); - } - - async element(selector: string): Promise { - return this.spectron.client.element(selector) - .then(result => result.value); - } - - async waitForActiveElement(selector: string): Promise { - return this.waitFor( - () => this.spectron.client.execute(s => document.activeElement.matches(s), selector), - r => r.value, - `wait for active element: ${selector}` - ); - } - - async waitForAttribute(selector: string, attribute: string, accept: (result: string) => boolean = result => !!result): Promise { - return this.waitFor(() => this.spectron.client.getAttribute(selector), accept, `attribute with selector ${selector}`); - } - - async dragAndDrop(sourceElem: string, destinationElem: string, capture: boolean = true): Promise { - return this.spectron.client.dragAndDrop(sourceElem, destinationElem); - } - - async selectByValue(selector: string, value: string, capture: boolean = true): Promise { - return this.spectron.client.selectByValue(selector, value); - } - - async getValue(selector: string, capture: boolean = true): Promise { - return this.spectron.client.getValue(selector); - } - - async getAttribute(selector: string, attribute: string, capture: boolean = true): Promise { - return Promise.resolve(this.spectron.client.getAttribute(selector, attribute)); - } - - buttonDown(): any { - return this.spectron.client.buttonDown(); - } - - buttonUp(): any { - return this.spectron.client.buttonUp(); - } - - async isVisible(selector: string, capture: boolean = true): Promise { - return this.spectron.client.isVisible(selector); - } - - async getTitle(): Promise { - return this.spectron.client.getTitle(); - } - - private running = false; - async waitFor(func: () => T | Promise, accept?: (result: T) => boolean | Promise, timeoutMessage?: string, retryCount?: number): Promise; - async waitFor(func: () => T | Promise, accept: (result: T) => boolean | Promise = result => !!result, timeoutMessage?: string, retryCount?: number): Promise { - if (this.running) { - throw new Error('Not allowed to run nested waitFor calls!'); - } - - this.running = true; - - try { - let trial = 1; - retryCount = typeof retryCount === 'number' ? retryCount : this.retryCount; - - while (true) { - if (trial > retryCount) { - await this.application.screenCapturer.capture('timeout'); - throw new Error(`${timeoutMessage}: Timed out after ${(retryCount * this.retryDuration) / 1000} seconds.`); - } - - let result; - try { - result = await func(); - } catch (e) { - // console.log(e); - } - - if (accept(result)) { - return result; - } - - await new Promise(resolve => setTimeout(resolve, this.retryDuration)); - trial++; - } - } finally { - this.running = false; - } - } - - // type(text: string): Promise { - // return new Promise((res) => { - // let textSplit = text.split(' '); - - // const type = async (i: number) => { - // if (!textSplit[i] || textSplit[i].length <= 0) { - // return res(); - // } - - // const toType = textSplit[i + 1] ? `${textSplit[i]} ` : textSplit[i]; - // await this.keys(toType); - // await this.keys(['NULL']); - // await type(i + 1); - // }; - - // return type(0); - // }); - // } -} \ No newline at end of file diff --git a/test/smoke/src/vscode/code.ts b/test/smoke/src/vscode/code.ts new file mode 100644 index 00000000000..c60984c6441 --- /dev/null +++ b/test/smoke/src/vscode/code.ts @@ -0,0 +1,324 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as path from 'path'; +import * as cp from 'child_process'; +import * as os from 'os'; +import { tmpName } from 'tmp'; +import { IDriver, connect as connectDriver, IDisposable, IElement } from './driver'; +import { Logger } from '../logger'; + +const repoPath = path.join(__dirname, '../../../..'); + +function getDevElectronPath(): string { + const buildPath = path.join(repoPath, '.build'); + const product = require(path.join(repoPath, 'product.json')); + + switch (process.platform) { + case 'darwin': + return path.join(buildPath, 'electron', `${product.nameLong}.app`, 'Contents', 'MacOS', 'Electron'); + case 'linux': + return path.join(buildPath, 'electron', `${product.applicationName}`); + case 'win32': + return path.join(buildPath, 'electron', `${product.nameShort}.exe`); + default: + throw new Error('Unsupported platform.'); + } +} + +function getBuildElectronPath(root: string): string { + switch (process.platform) { + case 'darwin': + return path.join(root, 'Contents', 'MacOS', 'Electron'); + case 'linux': { + const product = require(path.join(root, 'resources', 'app', 'product.json')); + return path.join(root, product.applicationName); + } + case 'win32': { + const product = require(path.join(root, 'resources', 'app', 'product.json')); + return path.join(root, `${product.nameShort}.exe`); + } + default: + throw new Error('Unsupported platform.'); + } +} + +function getDevOutPath(): string { + return path.join(repoPath, 'out'); +} + +function getBuildOutPath(root: string): string { + switch (process.platform) { + case 'darwin': + return path.join(root, 'Contents', 'Resources', 'app', 'out'); + default: + return path.join(root, 'resources', 'app', 'out'); + } +} + +async function connect(child: cp.ChildProcess, outPath: string, handlePath: string, logger: Logger): Promise { + let errCount = 0; + + while (true) { + try { + const { client, driver } = await connectDriver(outPath, handlePath); + return new Code(child, client, driver, logger); + } catch (err) { + if (++errCount > 50) { + child.kill(); + throw err; + } + + // retry + await new Promise(c => setTimeout(c, 100)); + } + } +} + +// Kill all running instances, when dead +const instances = new Set(); +process.once('exit', () => instances.forEach(code => code.kill())); + +export interface SpawnOptions { + codePath?: string; + workspacePath: string; + userDataDir: string; + extensionsPath: string; + logger: Logger; + extraArgs?: string[]; +} + +async function createDriverHandle(): Promise { + if ('win32' === os.platform()) { + const name = [...Array(15)].map(() => Math.random().toString(36)[3]).join(''); + return `\\\\.\\pipe\\${name}`; + } else { + return await new Promise((c, e) => tmpName((err, handlePath) => err ? e(err) : c(handlePath))); + } +} + +export async function spawn(options: SpawnOptions): Promise { + const codePath = options.codePath; + const electronPath = codePath ? getBuildElectronPath(codePath) : getDevElectronPath(); + const outPath = codePath ? getBuildOutPath(codePath) : getDevOutPath(); + const handle = await createDriverHandle(); + + const args = [ + options.workspacePath, + '--skip-getting-started', + '--skip-release-notes', + '--sticky-quickopen', + '--disable-telemetry', + '--disable-updates', + '--disable-crash-reporter', + `--extensions-dir=${options.extensionsPath}`, + `--user-data-dir=${options.userDataDir}`, + '--driver', handle + ]; + + if (!codePath) { + args.unshift(repoPath); + } + + if (options.extraArgs) { + args.push(...options.extraArgs); + } + + const spawnOptions: cp.SpawnOptions = {}; + + const child = cp.spawn(electronPath, args, spawnOptions); + + instances.add(child); + child.once('exit', () => instances.delete(child)); + + return connect(child, outPath, handle, options.logger); +} + +async function poll( + fn: () => Promise, + acceptFn: (result: T) => boolean, + timeoutMessage: string, + retryCount: number = 200, + retryInterval: number = 100 // millis +): Promise { + let trial = 1; + + while (true) { + if (trial > retryCount) { + throw new Error(`Timeout: ${timeoutMessage} after ${(retryCount * retryInterval) / 1000} seconds.`); + } + + let result; + try { + result = await fn(); + + if (acceptFn(result)) { + return result; + } + } catch (e) { + // console.warn(e); + + if (/Method not implemented/.test(e.message)) { + throw e; + } + } + + await new Promise(resolve => setTimeout(resolve, retryInterval)); + trial++; + } +} + +export class Code { + + private _activeWindowId: number | undefined = undefined; + private driver: IDriver; + + constructor( + private process: cp.ChildProcess, + private client: IDisposable, + driver: IDriver, + readonly logger: Logger + ) { + this.driver = new Proxy(driver, { + get(target, prop, receiver) { + if (typeof target[prop] !== 'function') { + return target[prop]; + } + + return function (...args) { + logger.log(`${prop}`, ...args.filter(a => typeof a === 'string')); + return target[prop].apply(this, args); + }; + } + }); + } + + async capturePage(): Promise { + const windowId = await this.getActiveWindowId(); + return await this.driver.capturePage(windowId); + } + + async waitForWindowIds(fn: (windowIds: number[]) => boolean): Promise { + await poll(() => this.driver.getWindowIds(), fn, `get window ids`); + } + + async dispatchKeybinding(keybinding: string): Promise { + const windowId = await this.getActiveWindowId(); + await this.driver.dispatchKeybinding(windowId, keybinding); + } + + async reload(): Promise { + const windowId = await this.getActiveWindowId(); + await this.driver.reloadWindow(windowId); + } + + async waitForTextContent(selector: string, textContent?: string, accept?: (result: string) => boolean): Promise { + const windowId = await this.getActiveWindowId(); + accept = accept || (result => textContent !== void 0 ? textContent === result : !!result); + return await poll(() => this.driver.getElements(windowId, selector).then(els => els[0].textContent), s => accept!(typeof s === 'string' ? s : ''), `get text content '${selector}'`); + } + + async waitAndClick(selector: string, xoffset?: number, yoffset?: number): Promise { + const windowId = await this.getActiveWindowId(); + await poll(() => this.driver.click(windowId, selector, xoffset, yoffset), () => true, `click '${selector}'`); + } + + async waitAndDoubleClick(selector: string): Promise { + const windowId = await this.getActiveWindowId(); + await poll(() => this.driver.doubleClick(windowId, selector), () => true, `double click '${selector}'`); + } + + async waitAndMove(selector: string): Promise { + const windowId = await this.getActiveWindowId(); + await poll(() => this.driver.move(windowId, selector), () => true, `move '${selector}'`); + } + + async waitForSetValue(selector: string, value: string): Promise { + const windowId = await this.getActiveWindowId(); + await poll(() => this.driver.setValue(windowId, selector, value), () => true, `set value '${selector}'`); + } + + async waitForPaste(selector: string, value: string): Promise { + const windowId = await this.getActiveWindowId(); + await poll(() => this.driver.paste(windowId, selector, value), () => true, `paste '${selector}'`); + } + + async waitForElements(selector: string, recursive: boolean, accept: (result: IElement[]) => boolean = result => result.length > 0): Promise { + const windowId = await this.getActiveWindowId(); + return await poll(() => this.driver.getElements(windowId, selector, recursive), accept, `get elements '${selector}'`); + } + + async waitForElement(selector: string, accept: (result: IElement | undefined) => boolean = result => !!result, retryCount: number = 200): Promise { + const windowId = await this.getActiveWindowId(); + return await poll(() => this.driver.getElements(windowId, selector).then(els => els[0]), accept, `get element '${selector}'`, retryCount); + } + + async waitForActiveElement(selector: string, retryCount: number = 200): Promise { + const windowId = await this.getActiveWindowId(); + await poll(() => this.driver.isActiveElement(windowId, selector), r => r, `is active element '${selector}'`, retryCount); + } + + async waitForTitle(fn: (title: string) => boolean): Promise { + const windowId = await this.getActiveWindowId(); + await poll(() => this.driver.getTitle(windowId), fn, `get title`); + } + + async waitForTypeInEditor(selector: string, text: string): Promise { + const windowId = await this.getActiveWindowId(); + await poll(() => this.driver.typeInEditor(windowId, selector, text), () => true, `type in editor '${selector}'`); + } + + async waitForTerminalBuffer(selector: string, accept: (result: string[]) => boolean): Promise { + const windowId = await this.getActiveWindowId(); + await poll(() => this.driver.getTerminalBuffer(windowId, selector), accept, `get terminal buffer '${selector}'`); + } + + private async getActiveWindowId(): Promise { + if (typeof this._activeWindowId !== 'number') { + const windows = await this.driver.getWindowIds(); + this._activeWindowId = windows[0]; + } + + return this._activeWindowId; + } + + dispose(): void { + this.client.dispose(); + this.process.kill(); + } +} + +export function findElement(element: IElement, fn: (element: IElement) => boolean): IElement | null { + const queue = [element]; + + while (queue.length > 0) { + const element = queue.shift()!; + + if (fn(element)) { + return element; + } + + queue.push(...element.children); + } + + return null; +} + +export function findElements(element: IElement, fn: (element: IElement) => boolean): IElement[] { + const result: IElement[] = []; + const queue = [element]; + + while (queue.length > 0) { + const element = queue.shift()!; + + if (fn(element)) { + result.push(element); + } + + queue.push(...element.children); + } + + return result; +} \ No newline at end of file diff --git a/test/smoke/src/vscode/driver.js b/test/smoke/src/vscode/driver.js new file mode 100644 index 00000000000..24af32d436f --- /dev/null +++ b/test/smoke/src/vscode/driver.js @@ -0,0 +1,12 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +const path = require('path'); + +exports.connect = function (outPath, handle) { + const bootstrapPath = path.join(outPath, 'bootstrap-amd.js'); + const { bootstrap } = require(bootstrapPath); + return new Promise((c, e) => bootstrap('vs/platform/driver/node/driver', ({ connect }) => connect(handle).then(c, e), e)); +}; \ No newline at end of file diff --git a/test/smoke/test/mocha.opts b/test/smoke/test/mocha.opts index 102d5b65ade..9f769655625 100644 --- a/test/smoke/test/mocha.opts +++ b/test/smoke/test/mocha.opts @@ -1,3 +1,3 @@ ---timeout 60000 +--timeout 20000 --slow 20000 out/main.js \ No newline at end of file diff --git a/test/smoke/tools/copy-driver-definition.js b/test/smoke/tools/copy-driver-definition.js new file mode 100644 index 00000000000..fedf0c28432 --- /dev/null +++ b/test/smoke/tools/copy-driver-definition.js @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +const fs = require('fs'); +const path = require('path'); + +const root = path.dirname(path.dirname(path.dirname(__dirname))); +const driverPath = path.join(root, 'src/vs/platform/driver/common/driver.ts'); + +let contents = fs.readFileSync(driverPath, 'utf8'); +contents = /\/\/\*START([\s\S]*)\/\/\*END/mi.exec(contents)[1].trim(); +contents = contents.replace(/\bTPromise\b/g, 'Promise'); + +contents = `/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +${contents} + +export interface IDisposable { + dispose(): void; +} + +export function connect(outPath: string, handle: string): Promise<{ client: IDisposable, driver: IDriver }>; +`; + +const srcPath = path.join(path.dirname(__dirname), 'src/vscode'); +const outDriverPath = path.join(srcPath, 'driver.d.ts'); + +fs.writeFileSync(outDriverPath, contents); \ No newline at end of file diff --git a/test/smoke/yarn.lock b/test/smoke/yarn.lock index 0508d361c3b..2f15aec3278 100644 --- a/test/smoke/yarn.lock +++ b/test/smoke/yarn.lock @@ -58,6 +58,17 @@ dependencies: "@types/node" "*" +abbrev@1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" + +ajv@^4.9.1: + version "4.11.8" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536" + dependencies: + co "^4.6.0" + json-stable-stringify "^1.0.1" + ajv@^5.1.0: version "5.3.0" resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.3.0.tgz#4414ff74a50879c208ee5fdc826e32c303549eda" @@ -67,56 +78,66 @@ ajv@^5.1.0: fast-json-stable-stringify "^2.0.0" json-schema-traverse "^0.3.0" -amdefine@>=0.0.4: - version "1.0.1" - resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5" - -ansi-escapes@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.0.0.tgz#ec3e8b4e9f8064fc02c3ac9b65f1c275bda8ef92" +ansi-regex@^0.2.0, ansi-regex@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-0.2.1.tgz#0d8e946967a3d8143f93e24e298525fc1b2235f9" ansi-regex@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" -ansi-regex@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" +ansi-styles@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-1.1.0.tgz#eaecbf66cd706882760b2f4691582b8f55d7a7de" -ansi-styles@^3.1.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.0.tgz#c159b8d5be0f9e5a6f346dab94f16ce022161b88" +anymatch@^1.3.0: + version "1.3.2" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-1.3.2.tgz#553dcb8f91e3c889845dfdba34c77721b90b9d7a" dependencies: - color-convert "^1.9.0" - -archiver-utils@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/archiver-utils/-/archiver-utils-1.3.0.tgz#e50b4c09c70bf3d680e32ff1b7994e9f9d895174" - dependencies: - glob "^7.0.0" - graceful-fs "^4.1.0" - lazystream "^1.0.0" - lodash "^4.8.0" + micromatch "^2.1.5" normalize-path "^2.0.0" - readable-stream "^2.0.0" -archiver@~2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/archiver/-/archiver-2.1.0.tgz#d2df2e8d5773a82c1dcce925ccc41450ea999afd" +aproba@^1.0.3: + version "1.2.0" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" + +are-we-there-yet@~1.1.2: + version "1.1.4" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz#bb5dca382bb94f05e15194373d16fd3ba1ca110d" dependencies: - archiver-utils "^1.3.0" - async "^2.0.0" - buffer-crc32 "^0.2.1" - glob "^7.0.0" - lodash "^4.8.0" - readable-stream "^2.0.0" - tar-stream "^1.5.0" - zip-stream "^1.2.0" + delegates "^1.0.0" + readable-stream "^2.0.6" + +arr-diff@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-2.0.0.tgz#8f3b827f955a8bd669697e4a4256ac3ceae356cf" + dependencies: + arr-flatten "^1.0.1" + +arr-flatten@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" + +array-filter@~0.0.0: + version "0.0.1" + resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-0.0.1.tgz#7da8cf2e26628ed732803581fd21f67cacd2eeec" array-find-index@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1" +array-map@~0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/array-map/-/array-map-0.0.0.tgz#88a2bab73d1cf7bcd5c1b118a003f66f665fa662" + +array-reduce@~0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/array-reduce/-/array-reduce-0.0.0.tgz#173899d3ffd1c7d9383e4479525dbe278cab5f2b" + +array-unique@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.2.1.tgz#a1d97ccafcbc2625cc70fadceb36a50c58b01a53" + asn1@~0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.3.tgz#dac8787713c9966849fc8180777ebe9c1ddf3b86" @@ -125,29 +146,35 @@ assert-plus@1.0.0, assert-plus@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" -async@^2.0.0: - version "2.6.0" - resolved "https://registry.yarnpkg.com/async/-/async-2.6.0.tgz#61a29abb6fcc026fea77e56d1c6ec53a795951f4" - dependencies: - lodash "^4.14.0" +assert-plus@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.2.0.tgz#d74e1b87e7affc0db8aadb7021f3fe48101ab234" + +async-each@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d" asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" -atob@~1.1.0: - version "1.1.3" - resolved "https://registry.yarnpkg.com/atob/-/atob-1.1.3.tgz#95f13629b12c3a51a5d215abdce2aa9f32f80773" +aws-sign2@~0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f" aws-sign2@~0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" +aws4@^1.2.1: + version "1.7.0" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.7.0.tgz#d4d0e9b9dbfca77bf08eeb0a8a471550fe39e289" + aws4@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e" -babel-runtime@^6.26.0: +babel-runtime@^6.9.2: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" dependencies: @@ -164,16 +191,26 @@ bcrypt-pbkdf@^1.0.0: dependencies: tweetnacl "^0.14.3" -bl@^1.0.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/bl/-/bl-1.2.1.tgz#cac328f7bee45730d404b692203fcb590e172d5e" +binary-extensions@^1.0.0: + version "1.11.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.11.0.tgz#46aa1751fb6a2f93ee5e689bb1087d4b14c6c205" + +block-stream@*: + version "0.0.9" + resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a" dependencies: - readable-stream "^2.0.5" + inherits "~2.0.0" bluebird@^2.9.34: version "2.11.0" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-2.11.0.tgz#534b9033c022c9579c56ba3b3e5a5caafbb650e1" +boom@2.x.x: + version "2.10.1" + resolved "https://registry.yarnpkg.com/boom/-/boom-2.10.1.tgz#39c8918ceff5799f83f9492a848f625add0c766f" + dependencies: + hoek "2.x.x" + boom@4.x.x: version "4.3.1" resolved "https://registry.yarnpkg.com/boom/-/boom-4.3.1.tgz#4f8a3005cb4a7e3889f749030fd25b96e01d2e31" @@ -193,14 +230,18 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" +braces@^1.8.2: + version "1.8.5" + resolved "https://registry.yarnpkg.com/braces/-/braces-1.8.5.tgz#ba77962e12dff969d6b76711e914b737857bf6a7" + dependencies: + expand-range "^1.8.1" + preserve "^0.2.0" + repeat-element "^1.1.2" + browser-stdout@1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.0.tgz#f351d32969d32fa5d7a5567154263d928ae3bd1f" -buffer-crc32@^0.2.1: - version "0.2.13" - resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" - builtin-modules@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" @@ -220,23 +261,30 @@ caseless@~0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" -chalk@^2.0.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.3.0.tgz#b5ea48efc9c1793dccc9b4767c93914d3f2d52ba" +chalk@0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-0.5.1.tgz#663b3a648b68b55d04690d49167aa837858f2174" dependencies: - ansi-styles "^3.1.0" - escape-string-regexp "^1.0.5" - supports-color "^4.0.0" + ansi-styles "^1.1.0" + escape-string-regexp "^1.0.0" + has-ansi "^0.1.0" + strip-ansi "^0.3.0" + supports-color "^0.2.0" -cli-cursor@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5" +chokidar@^1.6.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468" dependencies: - restore-cursor "^2.0.0" - -cli-width@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639" + anymatch "^1.3.0" + async-each "^1.0.0" + glob-parent "^2.0.0" + inherits "^2.0.1" + is-binary-path "^1.0.0" + is-glob "^2.0.0" + path-is-absolute "^1.0.0" + readdirp "^2.0.0" + optionalDependencies: + fsevents "^1.0.0" co@^4.6.0: version "4.6.0" @@ -246,22 +294,16 @@ code-point-at@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" -color-convert@^1.9.0: - version "1.9.1" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.1.tgz#c1261107aeb2f294ebffec9ed9ecad529a6097ed" - dependencies: - color-name "^1.1.1" - -color-name@^1.1.1: - version "1.1.3" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" - combined-stream@^1.0.5, combined-stream@~1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.5.tgz#938370a57b4a51dea2c77c15d5c5fdf895164009" dependencies: delayed-stream "~1.0.0" +commander@2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.6.0.tgz#9df7e52fb2a0cb0fb89058ee80c3104225f37e1d" + commander@2.9.0: version "2.9.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4" @@ -272,15 +314,6 @@ commander@^2.8.1: version "2.11.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.11.0.tgz#157152fd1e7a6c8d98a5b715cf376df928004563" -compress-commons@^1.2.0: - version "1.2.2" - resolved "https://registry.yarnpkg.com/compress-commons/-/compress-commons-1.2.2.tgz#524a9f10903f3a813389b0225d27c48bb751890f" - dependencies: - buffer-crc32 "^0.2.1" - crc32-stream "^2.0.0" - normalize-path "^2.0.0" - readable-stream "^2.0.0" - concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -293,6 +326,23 @@ concat-stream@1.6.0: readable-stream "^2.2.2" typedarray "^0.0.6" +concurrently@^3.5.1: + version "3.5.1" + resolved "https://registry.yarnpkg.com/concurrently/-/concurrently-3.5.1.tgz#ee8b60018bbe86b02df13e5249453c6ececd2521" + dependencies: + chalk "0.5.1" + commander "2.6.0" + date-fns "^1.23.0" + lodash "^4.5.1" + rx "2.3.24" + spawn-command "^0.0.2-1" + supports-color "^3.2.3" + tree-kill "^1.1.0" + +console-control-strings@^1.0.0, console-control-strings@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" + core-js@^2.4.0: version "2.5.1" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.1.tgz#ae6874dc66937789b80754ff5428df66819ca50b" @@ -301,16 +351,27 @@ core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" -crc32-stream@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/crc32-stream/-/crc32-stream-2.0.0.tgz#e3cdd3b4df3168dd74e3de3fbbcb7b297fe908f4" +cpx@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/cpx/-/cpx-1.5.0.tgz#185be018511d87270dedccc293171e37655ab88f" dependencies: - crc "^3.4.4" - readable-stream "^2.0.0" + babel-runtime "^6.9.2" + chokidar "^1.6.0" + duplexer "^0.1.1" + glob "^7.0.5" + glob2base "^0.0.12" + minimatch "^3.0.2" + mkdirp "^0.5.1" + resolve "^1.1.7" + safe-buffer "^5.0.1" + shell-quote "^1.6.1" + subarg "^1.0.0" -crc@^3.4.4: - version "3.5.0" - resolved "https://registry.yarnpkg.com/crc/-/crc-3.5.0.tgz#98b8ba7d489665ba3979f59b21381374101a1964" +cryptiles@2.x.x: + version "2.0.5" + resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8" + dependencies: + boom "2.x.x" cryptiles@3.x.x: version "3.1.2" @@ -318,25 +379,6 @@ cryptiles@3.x.x: dependencies: boom "5.x.x" -css-parse@~2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/css-parse/-/css-parse-2.0.0.tgz#a468ee667c16d81ccf05c58c38d2a97c780dbfd4" - dependencies: - css "^2.0.0" - -css-value@~0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/css-value/-/css-value-0.0.1.tgz#5efd6c2eea5ea1fd6b6ac57ec0427b18452424ea" - -css@^2.0.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/css/-/css-2.2.1.tgz#73a4c81de85db664d4ee674f7d47085e3b2d55dc" - dependencies: - inherits "^2.0.1" - source-map "^0.1.38" - source-map-resolve "^0.3.0" - urix "^0.1.0" - currently-unhandled@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" @@ -349,6 +391,10 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" +date-fns@^1.23.0: + version "1.29.0" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.29.0.tgz#12e609cdcb935127311d04d33334e2960a2a54e6" + debug@2.6.8: version "2.6.8" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.8.tgz#e731531ca2ede27d188222427da17821d68ff4fc" @@ -369,17 +415,17 @@ deep-extend@~0.4.0: version "0.4.2" resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.2.tgz#48b699c27e334bf89f10892be432f6e4c7d34a7f" -deepmerge@~2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-2.0.1.tgz#25c1c24f110fb914f80001b925264dd77f3f4312" - delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" -dev-null@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/dev-null/-/dev-null-0.1.1.tgz#5a205ce3c2b2ef77b6238d6ba179eb74c6a0e818" +delegates@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" + +detect-libc@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" diff@3.2.0: version "3.2.0" @@ -413,23 +459,16 @@ domutils@^1.5.1: dom-serializer "0" domelementtype "1" +duplexer@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1" + ecc-jsbn@~0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505" dependencies: jsbn "~0.1.0" -ejs@~2.5.6: - version "2.5.7" - resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.5.7.tgz#cc872c168880ae3c7189762fd5ffc00896c9518a" - -electron-chromedriver@~1.7.1: - version "1.7.1" - resolved "https://registry.yarnpkg.com/electron-chromedriver/-/electron-chromedriver-1.7.1.tgz#008c97976007aa4eb18491ee095e94d17ee47610" - dependencies: - electron-download "^4.1.0" - extract-zip "^1.6.5" - electron-download@^3.0.1: version "3.3.0" resolved "https://registry.yarnpkg.com/electron-download/-/electron-download-3.3.0.tgz#2cfd54d6966c019c4d49ad65fbe65cc9cdef68c8" @@ -444,20 +483,6 @@ electron-download@^3.0.1: semver "^5.3.0" sumchecker "^1.2.0" -electron-download@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/electron-download/-/electron-download-4.1.0.tgz#bf932c746f2f87ffcc09d1dd472f2ff6b9187845" - dependencies: - debug "^2.2.0" - env-paths "^1.0.0" - fs-extra "^2.0.0" - minimist "^1.2.0" - nugget "^2.0.0" - path-exists "^3.0.0" - rc "^1.1.2" - semver "^5.3.0" - sumchecker "^2.0.1" - electron@1.7.7: version "1.7.7" resolved "https://registry.yarnpkg.com/electron/-/electron-1.7.7.tgz#cfd89ca9eba79d763ac0b0c6dcc583792097b9b6" @@ -466,20 +491,10 @@ electron@1.7.7: electron-download "^3.0.1" extract-zip "^1.0.3" -end-of-stream@^1.0.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.0.tgz#7a90d833efda6cfa6eac0f4949dbb0fad3a63206" - dependencies: - once "^1.4.0" - entities@^1.1.1, entities@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0" -env-paths@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-1.0.0.tgz#4168133b42bb05c38a35b1ae4397c8298ab369e0" - error-ex@^1.2.0: version "1.3.1" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.1.tgz#f855a86ce61adc4e8621c3cda21e7a7612c3a8dc" @@ -490,23 +505,39 @@ es6-promise@^4.0.5: version "4.1.1" resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.1.1.tgz#8811e90915d9a0dba36274f0b242dbda78f9c92a" -escape-string-regexp@1.0.5, escape-string-regexp@^1.0.5: +escape-string-regexp@1.0.5, escape-string-regexp@^1.0.0: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" -extend@~3.0.1: +exec-sh@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.2.1.tgz#163b98a6e89e6b65b47c2a28d215bc1f63989c38" + dependencies: + merge "^1.1.3" + +expand-brackets@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-0.1.5.tgz#df07284e342a807cd733ac5af72411e581d1177b" + dependencies: + is-posix-bracket "^0.1.0" + +expand-range@^1.8.1: + version "1.8.2" + resolved "https://registry.yarnpkg.com/expand-range/-/expand-range-1.8.2.tgz#a299effd335fe2721ebae8e257ec79644fc85337" + dependencies: + fill-range "^2.1.0" + +extend@~3.0.0, extend@~3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444" -external-editor@^2.0.4: - version "2.0.5" - resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-2.0.5.tgz#52c249a3981b9ba187c7cacf5beb50bf1d91a6bc" +extglob@^0.3.1: + version "0.3.2" + resolved "https://registry.yarnpkg.com/extglob/-/extglob-0.3.2.tgz#2e18ff3d2f49ab2765cec9023f011daa8d8349a1" dependencies: - iconv-lite "^0.4.17" - jschardet "^1.4.2" - tmp "^0.0.33" + is-extglob "^1.0.0" -extract-zip@^1.0.3, extract-zip@^1.6.5: +extract-zip@^1.0.3: version "1.6.6" resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.6.6.tgz#1290ede8d20d0872b429fd3f351ca128ec5ef85c" dependencies: @@ -533,11 +564,23 @@ fd-slicer@~1.0.1: dependencies: pend "~1.2.0" -figures@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962" +filename-regex@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/filename-regex/-/filename-regex-2.0.1.tgz#c1c4b9bee3e09725ddb106b75c1e301fe2f18b26" + +fill-range@^2.1.0: + version "2.2.3" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-2.2.3.tgz#50b77dfd7e469bc7492470963699fe7a8485a723" dependencies: - escape-string-regexp "^1.0.5" + is-number "^2.1.0" + isobject "^2.0.0" + randomatic "^1.1.3" + repeat-element "^1.1.2" + repeat-string "^1.5.2" + +find-index@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/find-index/-/find-index-0.1.1.tgz#675d358b2ca3892d795a1ab47232f8b6e2e0dde4" find-up@^1.0.0: version "1.1.2" @@ -546,10 +589,28 @@ find-up@^1.0.0: path-exists "^2.0.0" pinkie-promise "^2.0.0" +for-in@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" + +for-own@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/for-own/-/for-own-0.1.5.tgz#5265c681a4f294dabbf17c9509b6763aa84510ce" + dependencies: + for-in "^1.0.1" + forever-agent@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" +form-data@~2.1.1: + version "2.1.4" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.1.4.tgz#33c183acf193276ecaa98143a69e94bfee1750d1" + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.5" + mime-types "^2.1.12" + form-data@~2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.1.tgz#6fb94fbd71885306d73d15cc497fe4cc4ecd44bf" @@ -568,22 +629,46 @@ fs-extra@^0.30.0: path-is-absolute "^1.0.0" rimraf "^2.2.8" -fs-extra@^2.0.0: - version "2.1.2" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-2.1.2.tgz#046c70163cef9aad46b0e4a7fa467fb22d71de35" - dependencies: - graceful-fs "^4.1.2" - jsonfile "^2.1.0" - fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" -gaze@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/gaze/-/gaze-1.1.2.tgz#847224677adb8870d679257ed3388fdb61e40105" +fsevents@^1.0.0: + version "1.1.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.1.3.tgz#11f82318f5fe7bb2cd22965a108e9306208216d8" dependencies: - globule "^1.0.0" + nan "^2.3.0" + node-pre-gyp "^0.6.39" + +fstream-ignore@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/fstream-ignore/-/fstream-ignore-1.0.5.tgz#9c31dae34767018fe1d249b24dada67d092da105" + dependencies: + fstream "^1.0.0" + inherits "2" + minimatch "^3.0.0" + +fstream@^1.0.0, fstream@^1.0.10, fstream@^1.0.2: + version "1.0.11" + resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.11.tgz#5c1fb1f117477114f0632a0eb4b71b3cb0fd3171" + dependencies: + graceful-fs "^4.1.2" + inherits "~2.0.0" + mkdirp ">=0.5 0" + rimraf "2" + +gauge@~2.7.3: + version "2.7.4" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" + dependencies: + aproba "^1.0.3" + console-control-strings "^1.0.0" + has-unicode "^2.0.0" + object-assign "^4.1.0" + signal-exit "^3.0.0" + string-width "^1.0.1" + strip-ansi "^3.0.1" + wide-align "^1.1.0" get-stdin@^4.0.1: version "4.0.1" @@ -595,6 +680,25 @@ getpass@^0.1.1: dependencies: assert-plus "^1.0.0" +glob-base@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4" + dependencies: + glob-parent "^2.0.0" + is-glob "^2.0.0" + +glob-parent@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-2.0.0.tgz#81383d72db054fcccf5336daa902f182f6edbb28" + dependencies: + is-glob "^2.0.0" + +glob2base@^0.0.12: + version "0.0.12" + resolved "https://registry.yarnpkg.com/glob2base/-/glob2base-0.0.12.tgz#9d419b3e28f12e83a362164a277055922c9c0d56" + dependencies: + find-index "^0.1.1" + glob@7.1.1: version "7.1.1" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8" @@ -606,7 +710,7 @@ glob@7.1.1: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^7.0.0, glob@^7.0.5, glob@~7.1.1: +glob@^7.0.5: version "7.1.2" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" dependencies: @@ -617,15 +721,7 @@ glob@^7.0.0, glob@^7.0.5, glob@~7.1.1: once "^1.3.0" path-is-absolute "^1.0.0" -globule@^1.0.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/globule/-/globule-1.2.0.tgz#1dc49c6822dd9e8a2fa00ba2a295006e8664bd09" - dependencies: - glob "~7.1.1" - lodash "~4.17.4" - minimatch "~3.0.2" - -graceful-fs@^4.1.0, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9: +graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9: version "4.1.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" @@ -637,10 +733,21 @@ growl@1.9.2: version "1.9.2" resolved "https://registry.yarnpkg.com/growl/-/growl-1.9.2.tgz#0ea7743715db8d8de2c5ede1775e1b45ac85c02f" +har-schema@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-1.0.5.tgz#d263135f43307c02c602afc8fe95970c0151369e" + har-schema@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" +har-validator@~4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-4.2.1.tgz#33481d0f1bbff600dd203d75812a6a5fba002e2a" + dependencies: + ajv "^4.9.1" + har-schema "^1.0.5" + har-validator@~5.0.3: version "5.0.3" resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.0.3.tgz#ba402c266194f15956ef15e0fcf242993f6a7dfd" @@ -648,13 +755,28 @@ har-validator@~5.0.3: ajv "^5.1.0" har-schema "^2.0.0" +has-ansi@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-0.1.0.tgz#84f265aae8c0e6a88a12d7022894b7568894c62e" + dependencies: + ansi-regex "^0.2.0" + has-flag@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa" -has-flag@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51" +has-unicode@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" + +hawk@3.1.3, hawk@~3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4" + dependencies: + boom "2.x.x" + cryptiles "2.x.x" + hoek "2.x.x" + sntp "1.x.x" hawk@~6.0.2: version "6.0.2" @@ -669,6 +791,10 @@ he@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd" +hoek@2.x.x: + version "2.16.3" + resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed" + hoek@4.x.x: version "4.2.0" resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.0.tgz#72d9d0754f7fe25ca2d01ad8f8f9a9449a89526d" @@ -692,6 +818,14 @@ htmlparser2@^3.9.2: inherits "^2.0.1" readable-stream "^2.0.2" +http-signature@~1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.1.1.tgz#df72e267066cd0ac67fb76adf8e134a8fbcf91bf" + dependencies: + assert-plus "^0.2.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + http-signature@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" @@ -700,10 +834,6 @@ http-signature@~1.2.0: jsprim "^1.2.2" sshpk "^1.7.0" -iconv-lite@^0.4.17: - version "0.4.19" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b" - indent-string@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80" @@ -717,7 +847,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3: +inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" @@ -725,35 +855,44 @@ ini@~1.3.0: version "1.3.4" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.4.tgz#0537cb79daf59b59a1a517dff706c86ec039162e" -inquirer@~3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-3.3.0.tgz#9dd2f2ad765dcab1ff0443b491442a20ba227dc9" - dependencies: - ansi-escapes "^3.0.0" - chalk "^2.0.0" - cli-cursor "^2.1.0" - cli-width "^2.0.0" - external-editor "^2.0.4" - figures "^2.0.0" - lodash "^4.3.0" - mute-stream "0.0.7" - run-async "^2.2.0" - rx-lite "^4.0.8" - rx-lite-aggregates "^4.0.8" - string-width "^2.1.0" - strip-ansi "^4.0.0" - through "^2.3.6" - is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" +is-binary-path@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898" + dependencies: + binary-extensions "^1.0.0" + +is-buffer@^1.1.5: + version "1.1.6" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" + is-builtin-module@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-1.0.0.tgz#540572d34f7ac3119f8f76c30cbc1b1e037affbe" dependencies: builtin-modules "^1.0.0" +is-dotfile@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.3.tgz#a6a2f32ffd2dfb04f5ca25ecd0f6b83cf798a1e1" + +is-equal-shallow@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz#2238098fc221de0bcfa5d9eac4c45d638aa1c534" + dependencies: + is-primitive "^2.0.0" + +is-extendable@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" + +is-extglob@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-1.0.0.tgz#ac468177c4943405a092fc8f29760c6ffc6206c0" + is-finite@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.0.2.tgz#cc6677695602be550ef11e8b4aa6305342b6d0aa" @@ -766,13 +905,31 @@ is-fullwidth-code-point@^1.0.0: dependencies: number-is-nan "^1.0.0" -is-fullwidth-code-point@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" +is-glob@^2.0.0, is-glob@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-2.0.1.tgz#d096f926a3ded5600f3fdfd91198cb0888c2d863" + dependencies: + is-extglob "^1.0.0" -is-promise@^2.1.0: +is-number@^2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f" + dependencies: + kind-of "^3.0.2" + +is-number@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" + dependencies: + kind-of "^3.0.2" + +is-posix-bracket@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz#3334dc79774368e92f016e6fbc0a88f5cd6e6bc4" + +is-primitive@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-primitive/-/is-primitive-2.0.0.tgz#207bab91638499c07b2adf240a41a87210034575" is-typedarray@~1.0.0: version "1.0.0" @@ -786,10 +943,16 @@ isarray@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" -isarray@~1.0.0: +isarray@1.0.0, isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" +isobject@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" + dependencies: + isarray "1.0.0" + isstream@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" @@ -798,10 +961,6 @@ jsbn@~0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" -jschardet@^1.4.2: - version "1.6.0" - resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-1.6.0.tgz#c7d1a71edcff2839db2f9ec30fc5d5ebd3c1a678" - json-schema-traverse@^0.3.0: version "0.3.1" resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340" @@ -810,6 +969,12 @@ json-schema@0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" +json-stable-stringify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af" + dependencies: + jsonify "~0.0.0" + json-stringify-safe@~5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" @@ -824,6 +989,10 @@ jsonfile@^2.1.0: optionalDependencies: graceful-fs "^4.1.6" +jsonify@~0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" + jsprim@^1.2.2: version "1.4.1" resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" @@ -833,18 +1002,24 @@ jsprim@^1.2.2: json-schema "0.2.3" verror "1.10.0" +kind-of@^3.0.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" + dependencies: + is-buffer "^1.1.5" + +kind-of@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57" + dependencies: + is-buffer "^1.1.5" + klaw@^1.0.0: version "1.3.1" resolved "https://registry.yarnpkg.com/klaw/-/klaw-1.3.1.tgz#4088433b46b3b1ba259d78785d8e96f73ba02439" optionalDependencies: graceful-fs "^4.1.9" -lazystream@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/lazystream/-/lazystream-1.0.0.tgz#f6995fe0f820392f61396be89462407bb77168e4" - dependencies: - readable-stream "^2.0.5" - load-json-file@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" @@ -902,9 +1077,9 @@ lodash.keys@^3.0.0: lodash.isarguments "^3.0.0" lodash.isarray "^3.0.0" -lodash@^4.14.0, lodash@^4.3.0, lodash@^4.8.0, lodash@~4.17.4: - version "4.17.4" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" +lodash@^4.5.1: + version "4.17.5" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.5.tgz#99a92d65c0272debe8c96b6057bc8fbfa3bed511" loud-rejection@^1.0.0: version "1.6.0" @@ -932,21 +1107,49 @@ meow@^3.1.0: redent "^1.0.0" trim-newlines "^1.0.0" +merge@^1.1.3: + version "1.2.0" + resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.0.tgz#7531e39d4949c281a66b8c5a6e0265e8b05894da" + +micromatch@^2.1.5: + version "2.3.11" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565" + dependencies: + arr-diff "^2.0.0" + array-unique "^0.2.1" + braces "^1.8.2" + expand-brackets "^0.1.4" + extglob "^0.3.1" + filename-regex "^2.0.0" + is-extglob "^1.0.0" + is-glob "^2.0.1" + kind-of "^3.0.2" + normalize-path "^2.0.1" + object.omit "^2.0.0" + parse-glob "^3.0.4" + regex-cache "^0.4.2" + mime-db@~1.30.0: version "1.30.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.30.0.tgz#74c643da2dd9d6a45399963465b26d5ca7d71f01" +mime-db@~1.33.0: + version "1.33.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db" + mime-types@^2.1.12, mime-types@~2.1.17: version "2.1.17" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.17.tgz#09d7a393f03e995a79f8af857b70a9e0ab16557a" dependencies: mime-db "~1.30.0" -mimic-fn@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.1.0.tgz#e667783d92e89dbd342818b5230b9d62a672ad18" +mime-types@~2.1.7: + version "2.1.18" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.18.tgz#6f323f60a83d11146f831ff11fd66e2fe5503bb8" + dependencies: + mime-db "~1.33.0" -minimatch@^3.0.2, minimatch@^3.0.4, minimatch@~3.0.2: +minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" dependencies: @@ -960,17 +1163,13 @@ minimist@^1.1.0, minimist@^1.1.3, minimist@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" -minimist@~0.0.1: - version "0.0.10" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" - mkdirp@0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.0.tgz#1d73076a6df986cd9344e15e71fcc05a4c9abf12" dependencies: minimist "0.0.8" -mkdirp@0.5.1, mkdirp@^0.5.1, mkdirp@~0.5.1: +mkdirp@0.5.1, "mkdirp@>=0.5 0", mkdirp@^0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" dependencies: @@ -997,14 +1196,37 @@ ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" -mute-stream@0.0.7: - version "0.0.7" - resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" +nan@^2.3.0: + version "2.10.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.10.0.tgz#96d0cd610ebd58d4b4de9cc0c6828cda99c7548f" ncp@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ncp/-/ncp-2.0.0.tgz#195a21d6c46e361d2fb1281ba38b91e9df7bdbb3" +node-pre-gyp@^0.6.39: + version "0.6.39" + resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.39.tgz#c00e96860b23c0e1420ac7befc5044e1d78d8649" + dependencies: + detect-libc "^1.0.2" + hawk "3.1.3" + mkdirp "^0.5.1" + nopt "^4.0.1" + npmlog "^4.0.2" + rc "^1.1.7" + request "2.81.0" + rimraf "^2.6.1" + semver "^5.3.0" + tar "^2.2.1" + tar-pack "^3.4.0" + +nopt@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d" + dependencies: + abbrev "1" + osenv "^0.1.4" + normalize-package-data@^2.3.2, normalize-package-data@^2.3.4: version "2.4.0" resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.4.0.tgz#12f95a307d58352075a04907b84ac8be98ac012f" @@ -1014,15 +1236,20 @@ normalize-package-data@^2.3.2, normalize-package-data@^2.3.4: semver "2 || 3 || 4 || 5" validate-npm-package-license "^3.0.1" -normalize-path@^2.0.0: +normalize-path@^2.0.0, normalize-path@^2.0.1: version "2.1.1" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" dependencies: remove-trailing-separator "^1.0.1" -npm-install-package@~2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/npm-install-package/-/npm-install-package-2.1.0.tgz#d7efe3cfcd7ab00614b896ea53119dc9ab259125" +npmlog@^4.0.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" + dependencies: + are-we-there-yet "~1.1.2" + console-control-strings "~1.1.0" + gauge "~2.7.3" + set-blocking "~2.0.0" nugget@^2.0.0: version "2.0.1" @@ -1040,11 +1267,11 @@ number-is-nan@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" -oauth-sign@~0.8.2: +oauth-sign@~0.8.1, oauth-sign@~0.8.2: version "0.8.2" resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" -object-assign@^4.0.1: +object-assign@^4.0.1, object-assign@^4.1.0: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" @@ -1052,29 +1279,43 @@ object-keys@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-0.4.0.tgz#28a6aae7428dd2c3a92f3d95f21335dd204e0336" -once@^1.3.0, once@^1.4.0: +object.omit@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-2.0.1.tgz#1a9c744829f39dbb858c76ca3579ae2a54ebd1fa" + dependencies: + for-own "^0.1.4" + is-extendable "^0.1.1" + +once@^1.3.0, once@^1.3.3: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" dependencies: wrappy "1" -onetime@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4" - dependencies: - mimic-fn "^1.0.0" +os-homedir@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" -optimist@~0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" - dependencies: - minimist "~0.0.1" - wordwrap "~0.0.2" - -os-tmpdir@~1.0.2: +os-tmpdir@^1.0.0, os-tmpdir@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" +osenv@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410" + dependencies: + os-homedir "^1.0.0" + os-tmpdir "^1.0.0" + +parse-glob@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/parse-glob/-/parse-glob-3.0.4.tgz#b2c376cfb11f35513badd173ef0bb6e3a388391c" + dependencies: + glob-base "^0.3.0" + is-dotfile "^1.0.0" + is-extglob "^1.0.0" + is-glob "^2.0.0" + parse-json@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" @@ -1087,14 +1328,14 @@ path-exists@^2.0.0, path-exists@^2.1.0: dependencies: pinkie-promise "^2.0.0" -path-exists@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" - path-is-absolute@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" +path-parse@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.5.tgz#3c1adf871ea9cd6c9431b6ea2bd74a0ff055c4c1" + path-type@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" @@ -1107,6 +1348,10 @@ pend@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" +performance-now@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5" + performance-now@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" @@ -1133,6 +1378,10 @@ portastic@^1.0.1: commander "^2.8.1" debug "^2.2.0" +preserve@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b" + pretty-bytes@^1.0.2: version "1.0.4" resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-1.0.4.tgz#0a22e8210609ad35542f8c8d5d2159aff0751c84" @@ -1144,6 +1393,10 @@ process-nextick-args@~1.0.6: version "1.0.7" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" +process-nextick-args@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa" + progress-stream@^1.1.0: version "1.2.0" resolved "https://registry.yarnpkg.com/progress-stream/-/progress-stream-1.2.0.tgz#2cd3cfea33ba3a89c9c121ec3347abe9ab125f77" @@ -1151,25 +1404,24 @@ progress-stream@^1.1.0: speedometer "~0.1.2" through2 "~0.2.3" -punycode@1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" - punycode@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" -q@~1.5.0: - version "1.5.1" - resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" +qs@~6.4.0: + version "6.4.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" qs@~6.5.1: version "6.5.1" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8" -querystring@0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" +randomatic@^1.1.3: + version "1.1.7" + resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-1.1.7.tgz#c7abe9cc8b87c0baa876b19fde83fd464797e38c" + dependencies: + is-number "^3.0.0" + kind-of "^4.0.0" rc@^1.1.2: version "1.2.2" @@ -1180,6 +1432,15 @@ rc@^1.1.2: minimist "^1.2.0" strip-json-comments "~2.0.1" +rc@^1.1.7: + version "1.2.6" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.6.tgz#eb18989c6d4f4f162c399f79ddd29f3835568092" + dependencies: + deep-extend "~0.4.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~2.0.1" + read-pkg-up@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02" @@ -1195,7 +1456,7 @@ read-pkg@^1.0.0: normalize-package-data "^2.3.2" path-type "^1.0.0" -readable-stream@^2.0.0, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.2.2: +readable-stream@^2.0.2, readable-stream@^2.2.2: version "2.3.3" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.3.tgz#368f2512d79f9d46fdfc71349ae7878bbc1eb95c" dependencies: @@ -1207,6 +1468,18 @@ readable-stream@^2.0.0, readable-stream@^2.0.2, readable-stream@^2.0.5, readable string_decoder "~1.0.3" util-deprecate "~1.0.1" +readable-stream@^2.0.6, readable-stream@^2.1.4: + version "2.3.6" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + readable-stream@~1.1.9: version "1.1.14" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" @@ -1216,6 +1489,15 @@ readable-stream@~1.1.9: isarray "0.0.1" string_decoder "~0.10.x" +readdirp@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.1.0.tgz#4ed0ad060df3073300c48440373f72d1cc642d78" + dependencies: + graceful-fs "^4.1.2" + minimatch "^3.0.2" + readable-stream "^2.0.2" + set-immediate-shim "^1.0.1" + redent@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde" @@ -1227,17 +1509,58 @@ regenerator-runtime@^0.11.0: version "0.11.0" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.0.tgz#7e54fe5b5ccd5d6624ea6255c3473be090b802e1" +regex-cache@^0.4.2: + version "0.4.4" + resolved "https://registry.yarnpkg.com/regex-cache/-/regex-cache-0.4.4.tgz#75bdc58a2a1496cec48a12835bc54c8d562336dd" + dependencies: + is-equal-shallow "^0.1.3" + remove-trailing-separator@^1.0.1: version "1.1.0" resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" +repeat-element@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.2.tgz#ef089a178d1483baae4d93eb98b4f9e4e11d990a" + +repeat-string@^1.5.2: + version "1.6.1" + resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" + repeating@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda" dependencies: is-finite "^1.0.0" -request@^2.45.0, request@^2.81.0, request@~2.83.0: +request@2.81.0: + version "2.81.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0" + dependencies: + aws-sign2 "~0.6.0" + aws4 "^1.2.1" + caseless "~0.12.0" + combined-stream "~1.0.5" + extend "~3.0.0" + forever-agent "~0.6.1" + form-data "~2.1.1" + har-validator "~4.2.1" + hawk "~3.1.3" + http-signature "~1.1.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.7" + oauth-sign "~0.8.1" + performance-now "^0.2.0" + qs "~6.4.0" + safe-buffer "^5.0.1" + stringstream "~0.0.4" + tough-cookie "~2.3.0" + tunnel-agent "^0.6.0" + uuid "^3.0.0" + +request@^2.45.0: version "2.83.0" resolved "https://registry.yarnpkg.com/request/-/request-2.83.0.tgz#ca0b65da02ed62935887808e6f510381034e3356" dependencies: @@ -1264,42 +1587,21 @@ request@^2.45.0, request@^2.81.0, request@~2.83.0: tunnel-agent "^0.6.0" uuid "^3.1.0" -resolve-url@~0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" - -restore-cursor@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf" +resolve@^1.1.7: + version "1.7.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.7.0.tgz#2bdf5374811207285df0df652b78f118ab8f3c5e" dependencies: - onetime "^2.0.0" - signal-exit "^3.0.2" + path-parse "^1.0.5" -rgb2hex@~0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/rgb2hex/-/rgb2hex-0.1.0.tgz#ccd55f860ae0c5c4ea37504b958e442d8d12325b" - -rimraf@^2.2.8, rimraf@^2.6.1: +rimraf@2, rimraf@^2.2.8, rimraf@^2.5.1, rimraf@^2.6.1: version "2.6.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36" dependencies: glob "^7.0.5" -run-async@^2.2.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0" - dependencies: - is-promise "^2.1.0" - -rx-lite-aggregates@^4.0.8: - version "4.0.8" - resolved "https://registry.yarnpkg.com/rx-lite-aggregates/-/rx-lite-aggregates-4.0.8.tgz#753b87a89a11c95467c4ac1626c4efc4e05c67be" - dependencies: - rx-lite "*" - -rx-lite@*, rx-lite@^4.0.8: - version "4.0.8" - resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-4.0.8.tgz#0b1e11af8bc44836f04a6407e92da42467b79444" +rx@2.3.24: + version "2.3.24" + resolved "https://registry.yarnpkg.com/rx/-/rx-2.3.24.tgz#14f950a4217d7e35daa71bbcbe58eff68ea4b2b7" safe-buffer@^5.0.1, safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.1" @@ -1309,7 +1611,24 @@ safe-buffer@^5.0.1, safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.4.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e" -signal-exit@^3.0.0, signal-exit@^3.0.2: +set-blocking@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + +set-immediate-shim@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61" + +shell-quote@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.6.1.tgz#f4781949cce402697127430ea3b3c5476f481767" + dependencies: + array-filter "~0.0.0" + array-map "~0.0.0" + array-reduce "~0.0.0" + jsonify "~0.0.0" + +signal-exit@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" @@ -1319,30 +1638,21 @@ single-line-log@^1.1.2: dependencies: string-width "^1.0.1" +sntp@1.x.x: + version "1.0.9" + resolved "https://registry.yarnpkg.com/sntp/-/sntp-1.0.9.tgz#6541184cc90aeea6c6e7b35e2659082443c66198" + dependencies: + hoek "2.x.x" + sntp@2.x.x: version "2.1.0" resolved "https://registry.yarnpkg.com/sntp/-/sntp-2.1.0.tgz#2c6cec14fedc2222739caf9b5c3d85d1cc5a2cc8" dependencies: hoek "4.x.x" -source-map-resolve@^0.3.0: - version "0.3.1" - resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.3.1.tgz#610f6122a445b8dd51535a2a71b783dfc1248761" - dependencies: - atob "~1.1.0" - resolve-url "~0.2.1" - source-map-url "~0.3.0" - urix "~0.1.0" - -source-map-url@~0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.3.0.tgz#7ecaf13b57bcd09da8a40c5d269db33799d4aaf9" - -source-map@^0.1.38: - version "0.1.43" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.1.43.tgz#c24bc146ca517c1471f5dacbe2571b2b7f9e3346" - dependencies: - amdefine ">=0.0.4" +spawn-command@^0.0.2-1: + version "0.0.2-1" + resolved "https://registry.yarnpkg.com/spawn-command/-/spawn-command-0.0.2-1.tgz#62f5e9466981c1b796dc5929937e11c9c6921bd0" spdx-correct@~1.0.0: version "1.0.2" @@ -1358,26 +1668,10 @@ spdx-license-ids@^1.0.2: version "1.2.2" resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz#c9df7a3424594ade6bd11900d596696dc06bac57" -spectron@^3.7.2: - version "3.7.2" - resolved "https://registry.yarnpkg.com/spectron/-/spectron-3.7.2.tgz#86f41306a9b70ed6ee1500f7f7d3adc389afb446" - dependencies: - dev-null "^0.1.1" - electron-chromedriver "~1.7.1" - request "^2.81.0" - split "^1.0.0" - webdriverio "^4.8.0" - speedometer@~0.1.2: version "0.1.4" resolved "https://registry.yarnpkg.com/speedometer/-/speedometer-0.1.4.tgz#9876dbd2a169d3115402d48e6ea6329c8816a50d" -split@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/split/-/split-1.0.1.tgz#605bd9be303aa59fb35f9229fbea0ddec9ea07d9" - dependencies: - through "2" - sshpk@^1.7.0: version "1.13.1" resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.13.1.tgz#512df6da6287144316dc4c18fe1cf1d940739be3" @@ -1392,7 +1686,7 @@ sshpk@^1.7.0: jsbn "~0.1.0" tweetnacl "~0.14.0" -string-width@^1.0.1: +string-width@^1.0.1, string-width@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" dependencies: @@ -1400,13 +1694,6 @@ string-width@^1.0.1: is-fullwidth-code-point "^1.0.0" strip-ansi "^3.0.0" -string-width@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" - dependencies: - is-fullwidth-code-point "^2.0.0" - strip-ansi "^4.0.0" - string_decoder@~0.10.x: version "0.10.31" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" @@ -1417,22 +1704,28 @@ string_decoder@~1.0.3: dependencies: safe-buffer "~5.1.0" -stringstream@~0.0.5: +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + dependencies: + safe-buffer "~5.1.0" + +stringstream@~0.0.4, stringstream@~0.0.5: version "0.0.5" resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878" -strip-ansi@^3.0.0: +strip-ansi@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-0.3.0.tgz#25f48ea22ca79187f3174a4db8759347bb126220" + dependencies: + ansi-regex "^0.2.1" + +strip-ansi@^3.0.0, strip-ansi@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" dependencies: ansi-regex "^2.0.0" -strip-ansi@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" - dependencies: - ansi-regex "^3.0.0" - strip-bom@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" @@ -1449,6 +1742,12 @@ strip-json-comments@^2.0.1, strip-json-comments@~2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" +subarg@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/subarg/-/subarg-1.0.0.tgz#f62cf17581e996b48fc965699f54c06ae268b8d2" + dependencies: + minimist "^1.1.0" + sumchecker@^1.2.0: version "1.3.1" resolved "https://registry.yarnpkg.com/sumchecker/-/sumchecker-1.3.1.tgz#79bb3b4456dd04f18ebdbc0d703a1d1daec5105d" @@ -1456,38 +1755,42 @@ sumchecker@^1.2.0: debug "^2.2.0" es6-promise "^4.0.5" -sumchecker@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/sumchecker/-/sumchecker-2.0.2.tgz#0f42c10e5d05da5d42eea3e56c3399a37d6c5b3e" - dependencies: - debug "^2.2.0" - supports-color@3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.1.2.tgz#72a262894d9d408b956ca05ff37b2ed8a6e2a2d5" dependencies: has-flag "^1.0.0" -supports-color@^4.0.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.5.0.tgz#be7a0de484dec5c5cddf8b3d59125044912f635b" - dependencies: - has-flag "^2.0.0" +supports-color@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-0.2.0.tgz#d92de2694eb3f67323973d7ae3d8b55b4c22190a" -supports-color@~5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.0.0.tgz#1db26229f6ae02f9acdb5410907c36ce2e362b13" +supports-color@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.2.3.tgz#65ac0504b3954171d8a64946b2ae3cbb8a5f54f6" dependencies: - has-flag "^2.0.0" + has-flag "^1.0.0" -tar-stream@^1.5.0: - version "1.5.5" - resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-1.5.5.tgz#5cad84779f45c83b1f2508d96b09d88c7218af55" +tar-pack@^3.4.0: + version "3.4.1" + resolved "https://registry.yarnpkg.com/tar-pack/-/tar-pack-3.4.1.tgz#e1dbc03a9b9d3ba07e896ad027317eb679a10a1f" dependencies: - bl "^1.0.0" - end-of-stream "^1.0.0" - readable-stream "^2.0.0" - xtend "^4.0.0" + debug "^2.2.0" + fstream "^1.0.10" + fstream-ignore "^1.0.5" + once "^1.3.3" + readable-stream "^2.1.4" + rimraf "^2.5.1" + tar "^2.2.1" + uid-number "^0.0.6" + +tar@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.1.tgz#8e4d2a256c0e2185c6b18ad694aec968b83cb1d1" + dependencies: + block-stream "*" + fstream "^1.0.2" + inherits "2" throttleit@0.0.2: version "0.0.2" @@ -1500,22 +1803,28 @@ through2@~0.2.3: readable-stream "~1.1.9" xtend "~2.1.1" -through@2, through@^2.3.6: - version "2.3.8" - resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" - -tmp@0.0.33, tmp@^0.0.33: +tmp@0.0.33: version "0.0.33" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" dependencies: os-tmpdir "~1.0.2" +tough-cookie@~2.3.0: + version "2.3.4" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.4.tgz#ec60cee38ac675063ffc97a5c18970578ee83655" + dependencies: + punycode "^1.4.1" + tough-cookie@~2.3.3: version "2.3.3" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.3.tgz#0b618a5565b6dea90bf3425d04d55edc475a7561" dependencies: punycode "^1.4.1" +tree-kill@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.0.tgz#5846786237b4239014f05db156b643212d4c6f36" + trim-newlines@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" @@ -1538,21 +1847,18 @@ typescript@2.5.2: version "2.5.2" resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.5.2.tgz#038a95f7d9bbb420b1bf35ba31d4c5c1dd3ffe34" -urix@^0.1.0, urix@~0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" - -url@~0.11.0: - version "0.11.0" - resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" - dependencies: - punycode "1.3.2" - querystring "0.2.0" +uid-number@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81" util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" +uuid@^3.0.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.2.1.tgz#12c528bb9d58d0b9265d9a2f6f0fe8be17ff1f14" + uuid@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04" @@ -1564,10 +1870,6 @@ validate-npm-package-license@^3.0.1: spdx-correct "~1.0.0" spdx-expression-parse "~1.0.0" -validator@~9.1.1: - version "9.1.1" - resolved "https://registry.yarnpkg.com/validator/-/validator-9.1.1.tgz#3bdd1065cbd28f9d96ac806dee01030d32fd97ef" - verror@1.10.0: version "1.10.0" resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" @@ -1576,53 +1878,23 @@ verror@1.10.0: core-util-is "1.0.2" extsprintf "^1.2.0" -wdio-dot-reporter@~0.0.8: - version "0.0.9" - resolved "https://registry.yarnpkg.com/wdio-dot-reporter/-/wdio-dot-reporter-0.0.9.tgz#929b2adafd49d6b0534fda068e87319b47e38fe5" - -webdriverio@^4.8.0: - version "4.9.8" - resolved "https://registry.yarnpkg.com/webdriverio/-/webdriverio-4.9.8.tgz#907180e715d3b9e16cabe20bad59854bec1e44fa" +watch@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/watch/-/watch-1.0.2.tgz#340a717bde765726fa0aa07d721e0147a551df0c" dependencies: - archiver "~2.1.0" - babel-runtime "^6.26.0" - css-parse "~2.0.0" - css-value "~0.0.1" - deepmerge "~2.0.1" - ejs "~2.5.6" - gaze "~1.1.2" - glob "~7.1.1" - inquirer "~3.3.0" - json-stringify-safe "~5.0.1" - mkdirp "~0.5.1" - npm-install-package "~2.1.0" - optimist "~0.6.1" - q "~1.5.0" - request "~2.83.0" - rgb2hex "~0.1.0" - safe-buffer "~5.1.1" - supports-color "~5.0.0" - url "~0.11.0" - validator "~9.1.1" - wdio-dot-reporter "~0.0.8" - wgxpath "~1.0.0" + exec-sh "^0.2.0" + minimist "^1.2.0" -wgxpath@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/wgxpath/-/wgxpath-1.0.0.tgz#eef8a4b9d558cc495ad3a9a2b751597ecd9af690" - -wordwrap@~0.0.2: - version "0.0.3" - resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" +wide-align@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.2.tgz#571e0f1b0604636ebc0dfc21b0339bbe31341710" + dependencies: + string-width "^1.0.2" wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" -xtend@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" - xtend@~2.1.1: version "2.1.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-2.1.2.tgz#6efecc2a4dad8e6962c4901b337ce7ba87b5d28b" @@ -1634,12 +1906,3 @@ yauzl@2.4.1: resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.4.1.tgz#9528f442dab1b2284e58b4379bb194e22e0c4005" dependencies: fd-slicer "~1.0.1" - -zip-stream@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/zip-stream/-/zip-stream-1.2.0.tgz#a8bc45f4c1b49699c6b90198baacaacdbcd4ba04" - dependencies: - archiver-utils "^1.3.0" - compress-commons "^1.2.0" - lodash "^4.8.0" - readable-stream "^2.0.0" diff --git a/tslint.json b/tslint.json index 31396c2aa56..6e26e8166d0 100644 --- a/tslint.json +++ b/tslint.json @@ -409,6 +409,18 @@ "*" // node modules ] }, + { + "target": "**/vs/code/electron-browser/**", + "restrictions": [ + "vs/nls", + "vs/css!./**/*", + "vs/nls", + "**/vs/base/**", + "**/vs/platform/**", + "**/vs/code/**", + "*" // node modules + ] + }, { "target": "**/vs/code/**", "restrictions": [ @@ -430,6 +442,13 @@ "*" ] }, + { + "target": "**/test/smoke2/**", + "restrictions": [ + "**/test/smoke2/**", + "*" + ] + }, { "target": "{**/**.test.ts,**/test/**}", "restrictions": "{**/vs/**,assert,sinon,crypto}" diff --git a/yarn.lock b/yarn.lock index 29d519dd7b8..139077c5c6b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -602,10 +602,6 @@ chalk@^2.3.0: escape-string-regexp "^1.0.5" supports-color "^4.0.0" -charenc@~0.0.1: - version "0.0.2" - resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" - cheerio@^1.0.0-rc.1: version "1.0.0-rc.2" resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.2.tgz#4b9f53a81b27e4d5dac31c0ffd0cfa03cc6830db" @@ -885,10 +881,6 @@ cross-spawn@^5.0.1: shebang-command "^1.2.0" which "^1.2.9" -crypt@~0.0.1: - version "0.0.2" - resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" - cryptiles@0.2.x: version "0.2.2" resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-0.2.2.tgz#ed91ff1f17ad13d3748288594f8a48a0d26f325c" @@ -2753,10 +2745,6 @@ is-buffer@^1.0.2: version "1.1.4" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.4.tgz#cfc86ccd5dc5a52fa80489111c6920c457e2d98b" -is-buffer@~1.1.1: - version "1.1.6" - resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" - is-builtin-module@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-1.0.0.tgz#540572d34f7ac3119f8f76c30cbc1b1e037affbe" @@ -3510,14 +3498,6 @@ math-expression-evaluator@^1.2.14: version "1.2.17" resolved "https://registry.yarnpkg.com/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz#de819fdbcd84dccd8fae59c6aeb79615b9d266ac" -md5@^2.1.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/md5/-/md5-2.2.1.tgz#53ab38d5fe3c8891ba465329ea23fac0540126f9" - dependencies: - charenc "~0.0.1" - crypt "~0.0.1" - is-buffer "~1.1.1" - mdurl@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" @@ -3671,16 +3651,6 @@ mksnapshot@^0.3.0: fs-extra "0.26.7" request "^2.79.0" -mocha-junit-reporter@^1.17.0: - version "1.17.0" - resolved "https://registry.yarnpkg.com/mocha-junit-reporter/-/mocha-junit-reporter-1.17.0.tgz#2e5149ed40fc5d2e3ca71e42db5ab1fec9c6d85c" - dependencies: - debug "^2.2.0" - md5 "^2.1.0" - mkdirp "~0.5.1" - strip-ansi "^4.0.0" - xml "^1.0.0" - mocha@^2.0.1, mocha@^2.2.5: version "2.5.3" resolved "https://registry.yarnpkg.com/mocha/-/mocha-2.5.3.tgz#161be5bdeb496771eb9b35745050b622b5aefc58" @@ -5583,9 +5553,9 @@ typescript-formatter@7.1.0: commandpost "^1.0.0" editorconfig "^0.15.0" -typescript@2.7.2: - version "2.7.2" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.7.2.tgz#2d615a1ef4aee4f574425cdff7026edf81919836" +typescript@2.8.1: + version "2.8.1" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.8.1.tgz#6160e4f8f195d5ba81d4876f9c0cc1fbc0820624" typescript@^2.6.2: version "2.6.2" @@ -5880,9 +5850,9 @@ vscode-chokidar@1.6.2: optionalDependencies: vscode-fsevents "0.3.8" -vscode-debugprotocol@1.27.0: - version "1.27.0" - resolved "https://registry.yarnpkg.com/vscode-debugprotocol/-/vscode-debugprotocol-1.27.0.tgz#735a43a3cc1235fe587c0ef93fe4e328def7b17c" +vscode-debugprotocol@1.28.0: + version "1.28.0" + resolved "https://registry.yarnpkg.com/vscode-debugprotocol/-/vscode-debugprotocol-1.28.0.tgz#b9fb97c3fb2dadbec78e5c1619ff12bf741ce406" vscode-fsevents@0.3.8: version "0.3.8" @@ -5920,16 +5890,16 @@ vscode-ripgrep@^0.8.1: version "0.8.1" resolved "https://registry.yarnpkg.com/vscode-ripgrep/-/vscode-ripgrep-0.8.1.tgz#861d2ac97a3764e9f40f305620423efc50632ad1" -vscode-textmate@^3.3.2: - version "3.3.2" - resolved "https://registry.yarnpkg.com/vscode-textmate/-/vscode-textmate-3.3.2.tgz#43d7722d24ed168d195a1e3c582c6914917a37ab" +vscode-textmate@^3.3.3: + version "3.3.3" + resolved "https://registry.yarnpkg.com/vscode-textmate/-/vscode-textmate-3.3.3.tgz#8d737f2965046503a4088d47c2793aebe246d355" dependencies: fast-plist "^0.1.2" oniguruma "^6.0.1" -vscode-xterm@3.3.0-beta8: - version "3.3.0-beta8" - resolved "https://registry.yarnpkg.com/vscode-xterm/-/vscode-xterm-3.3.0-beta8.tgz#092403d293250e09a38d89a53ce120135e92c778" +vscode-xterm@3.4.0-beta3: + version "3.4.0-beta3" + resolved "https://registry.yarnpkg.com/vscode-xterm/-/vscode-xterm-3.4.0-beta3.tgz#349db387bd3669ad4d6044a6ea6d699e6d649fb5" vso-node-api@^6.1.2-preview: version "6.1.2-preview" @@ -5969,9 +5939,9 @@ windows-mutex@^0.2.0: bindings "^1.2.1" nan "^2.1.0" -windows-process-tree@0.1.6: - version "0.1.6" - resolved "https://registry.yarnpkg.com/windows-process-tree/-/windows-process-tree-0.1.6.tgz#c2d942a944152ea749a4c1c0bdb769b2f570639f" +windows-process-tree@0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/windows-process-tree/-/windows-process-tree-0.2.1.tgz#d750f8592bd956e89f8dc565bc47be6430d3df6e" dependencies: nan "^2.6.2" @@ -6029,10 +5999,6 @@ xml2js@^0.4.19: sax ">=0.6.0" xmlbuilder "~9.0.1" -xml@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/xml/-/xml-1.0.1.tgz#78ba72020029c5bc87b8a81a3cfcd74b4a2fc1e5" - xmlbuilder@0.4.3: version "0.4.3" resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-0.4.3.tgz#c4614ba74e0ad196e609c9272cd9e1ddb28a8a58"