diff --git a/.travis.yml b/.travis.yml index 02d0fb00dcc..6bedf740e15 100644 --- a/.travis.yml +++ b/.travis.yml @@ -42,8 +42,8 @@ script: - gulp electron --silent - gulp compile --silent - gulp optimize-vscode --silent - - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then ./scripts/test.sh --reporter dot --coverage; else ./scripts/test.sh --reporter dot; fi + - if [[ "$TRAVIS_OS_NAME" == "linux_off" ]]; then ./scripts/test.sh --reporter dot --coverage; else ./scripts/test.sh --reporter dot; fi - ./scripts/test-integration.sh after_success: - - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then node_modules/.bin/coveralls < .build/coverage/lcov.info; fi \ No newline at end of file + - if [[ "$TRAVIS_OS_NAME" == "linux_off" ]]; then node_modules/.bin/coveralls < .build/coverage/lcov.info; fi \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 83209bcf4a4..f0c713256fe 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -47,7 +47,7 @@ ], "severity": "warning", "pattern": { - "regexp": "(.*):(\\d+):(\\d+):(.*)$", + "regexp": "(.*)\\[(\\d+),\\s(\\d+)\\]:\\s(.*)$", // (.*)\[(\d+), (\d+)\]: (.*) "file": 1, "line": 2, "column": 3, diff --git a/build/gulpfile.vscode.js b/build/gulpfile.vscode.js index 404b81bfff5..575635e629d 100644 --- a/build/gulpfile.vscode.js +++ b/build/gulpfile.vscode.js @@ -39,8 +39,8 @@ const nodeModules = ['electron', 'original-fs'] // Build const builtInExtensions = [ - { name: 'ms-vscode.node-debug', version: '1.9.1' }, - { name: 'ms-vscode.node-debug2', version: '1.9.1' } + { name: 'ms-vscode.node-debug', version: '1.9.2' }, + { name: 'ms-vscode.node-debug2', version: '1.9.3' } ]; const vscodeEntryPoints = _.flatten([ diff --git a/build/lib/bundle.js b/build/lib/bundle.js index 9325d6d623f..78375d51eb6 100644 --- a/build/lib/bundle.js +++ b/build/lib/bundle.js @@ -3,9 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; -var fs = require('fs'); -var path = require('path'); -var vm = require('vm'); +var fs = require("fs"); +var path = require("path"); +var vm = require("vm"); /** * Bundle `entryPoints` given config `config`. */ @@ -186,7 +186,7 @@ function extractStrings(destFiles) { path: null, contents: [ '(function() {', - ("var __m = " + JSON.stringify(sortedByUseModules) + ";"), + "var __m = " + JSON.stringify(sortedByUseModules) + ";", "var __M = function(deps) {", " var result = [];", " for (var i = 0, len = deps.length; i < len; i++) {", diff --git a/build/lib/compilation.js b/build/lib/compilation.js index da18bdcbc63..2171ffa1ae9 100644 --- a/build/lib/compilation.js +++ b/build/lib/compilation.js @@ -3,19 +3,19 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ 'use strict'; -var gulp = require('gulp'); -var tsb = require('gulp-tsb'); -var es = require('event-stream'); +var gulp = require("gulp"); +var tsb = require("gulp-tsb"); +var es = require("event-stream"); var watch = require('./watch'); -var nls = require('./nls'); -var util = require('./util'); -var reporter_1 = require('./reporter'); -var path = require('path'); -var bom = require('gulp-bom'); -var sourcemaps = require('gulp-sourcemaps'); -var _ = require('underscore'); -var monacodts = require('../monaco/api'); -var fs = require('fs'); +var nls = require("./nls"); +var util = require("./util"); +var reporter_1 = require("./reporter"); +var path = require("path"); +var bom = require("gulp-bom"); +var sourcemaps = require("gulp-sourcemaps"); +var _ = require("underscore"); +var monacodts = require("../monaco/api"); +var fs = require("fs"); var reporter = reporter_1.createReporter(); var rootDir = path.join(__dirname, '../../src'); var options = require('../../src/tsconfig.json').compilerOptions; @@ -23,6 +23,8 @@ options.verbose = false; options.sourceMap = true; options.rootDir = rootDir; options.sourceRoot = util.toFileUri(rootDir); +var smSourceRootPath = path.resolve(path.dirname(rootDir)); +var smSourceRoot = util.toFileUri(smSourceRootPath); function createCompile(build, emitError) { var opts = _.clone(options); opts.inlineSources = !!build; @@ -46,7 +48,7 @@ function createCompile(build, emitError) { .pipe(sourcemaps.write('.', { addComment: false, includeContent: !!build, - sourceRoot: options.sourceRoot + sourceRoot: smSourceRoot })) .pipe(tsFilter.restore) .pipe(reporter.end(emitError)); diff --git a/build/lib/compilation.ts b/build/lib/compilation.ts index eed6a156cbd..ac367a573b0 100644 --- a/build/lib/compilation.ts +++ b/build/lib/compilation.ts @@ -28,14 +28,17 @@ options.sourceMap = true; options.rootDir = rootDir; options.sourceRoot = util.toFileUri(rootDir); -function createCompile(build:boolean, emitError?:boolean): (token?:util.ICancellationToken) => NodeJS.ReadWriteStream { +const smSourceRootPath = path.resolve(path.dirname(rootDir)); +const smSourceRoot = util.toFileUri(smSourceRootPath); + +function createCompile(build: boolean, emitError?: boolean): (token?: util.ICancellationToken) => NodeJS.ReadWriteStream { const opts = _.clone(options); opts.inlineSources = !!build; opts.noFilesystemLookup = true; const ts = tsb.create(opts, null, null, err => reporter(err.toString())); - return function (token?:util.ICancellationToken) { + return function (token?: util.ICancellationToken) { const utf8Filter = util.filter(data => /(\/|\\)test(\/|\\).*utf8/.test(data.path)); const tsFilter = util.filter(data => /\.ts$/.test(data.path)); const noDeclarationsFilter = util.filter(data => !(/\.d\.ts$/.test(data.path))); @@ -54,7 +57,7 @@ function createCompile(build:boolean, emitError?:boolean): (token?:util.ICancell .pipe(sourcemaps.write('.', { addComment: false, includeContent: !!build, - sourceRoot: options.sourceRoot + sourceRoot: smSourceRoot })) .pipe(tsFilter.restore) .pipe(reporter.end(emitError)); @@ -63,7 +66,7 @@ function createCompile(build:boolean, emitError?:boolean): (token?:util.ICancell }; } -export function compileTask(out:string, build:boolean): () => NodeJS.ReadWriteStream { +export function compileTask(out: string, build: boolean): () => NodeJS.ReadWriteStream { const compile = createCompile(build, true); return function () { @@ -79,7 +82,7 @@ export function compileTask(out:string, build:boolean): () => NodeJS.ReadWriteSt }; } -export function watchTask(out:string, build:boolean): () => NodeJS.ReadWriteStream { +export function watchTask(out: string, build: boolean): () => NodeJS.ReadWriteStream { const compile = createCompile(build); return function () { @@ -96,21 +99,21 @@ export function watchTask(out:string, build:boolean): () => NodeJS.ReadWriteStre }; } -function monacodtsTask(out:string, isWatch:boolean): NodeJS.ReadWriteStream { - let timer:NodeJS.Timer = null; +function monacodtsTask(out: string, isWatch: boolean): NodeJS.ReadWriteStream { + let timer: NodeJS.Timer = null; - const runSoon = function(howSoon:number) { + const runSoon = function (howSoon: number) { if (timer !== null) { clearTimeout(timer); timer = null; } - timer = setTimeout(function() { + timer = setTimeout(function () { timer = null; runNow(); }, howSoon); }; - const runNow = function() { + const runNow = function () { if (timer !== null) { clearTimeout(timer); timer = null; @@ -133,16 +136,16 @@ function monacodtsTask(out:string, isWatch:boolean): NodeJS.ReadWriteStream { if (isWatch) { - const filesToWatchMap: {[file:string]:boolean;} = {}; - monacodts.getFilesToWatch(out).forEach(function(filePath) { + const filesToWatchMap: { [file: string]: boolean; } = {}; + monacodts.getFilesToWatch(out).forEach(function (filePath) { filesToWatchMap[path.normalize(filePath)] = true; }); - watch('build/monaco/*').pipe(es.through(function() { + watch('build/monaco/*').pipe(es.through(function () { runSoon(5000); })); - resultStream = es.through(function(data) { + resultStream = es.through(function (data) { const filePath = path.normalize(data.path); if (filesToWatchMap[filePath]) { runSoon(5000); @@ -152,7 +155,7 @@ function monacodtsTask(out:string, isWatch:boolean): NodeJS.ReadWriteStream { } else { - resultStream = es.through(null, function() { + resultStream = es.through(null, function () { runNow(); this.emit('end'); }); diff --git a/build/lib/extensions.js b/build/lib/extensions.js index 70d38f3a5da..f2da7d5fd70 100644 --- a/build/lib/extensions.js +++ b/build/lib/extensions.js @@ -3,9 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; -var event_stream_1 = require('event-stream'); -var assign = require('object-assign'); -var remote = require('gulp-remote-src'); +var event_stream_1 = require("event-stream"); +var assign = require("object-assign"); +var remote = require("gulp-remote-src"); var flatmap = require('gulp-flatmap'); var vzip = require('gulp-vinyl-zip'); var filter = require('gulp-filter'); diff --git a/build/lib/git.js b/build/lib/git.js index 973f52ac38e..67163de17de 100644 --- a/build/lib/git.js +++ b/build/lib/git.js @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; -var path = require('path'); -var fs = require('fs'); +var path = require("path"); +var fs = require("fs"); /** * Returns the sha1 commit version of a repository or undefined in case of failure. */ diff --git a/build/lib/i18n.js b/build/lib/i18n.js index 9585915e14f..6af36a8ea48 100644 --- a/build/lib/i18n.js +++ b/build/lib/i18n.js @@ -3,11 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; -var path = require('path'); -var fs = require('fs'); -var event_stream_1 = require('event-stream'); -var File = require('vinyl'); -var Is = require('is'); +var path = require("path"); +var fs = require("fs"); +var event_stream_1 = require("event-stream"); +var File = require("vinyl"); +var Is = require("is"); var util = require('gulp-util'); function log(message) { var rest = []; @@ -233,7 +233,7 @@ function processCoreBundleFormat(fileHeader, json, emitter) { var modules = bundleSection[bundle]; var contents = [ fileHeader, - ("define(\"" + bundle + ".nls." + language.iso639_2 + "\", {") + "define(\"" + bundle + ".nls." + language.iso639_2 + "\", {" ]; modules.forEach(function (module, index) { contents.push("\t\"" + module + "\": ["); diff --git a/build/lib/nls.js b/build/lib/nls.js index 305b2c1487d..e695200d1bd 100644 --- a/build/lib/nls.js +++ b/build/lib/nls.js @@ -1,11 +1,11 @@ "use strict"; -var ts = require('./typescript/typescriptServices'); -var lazy = require('lazy.js'); -var event_stream_1 = require('event-stream'); -var File = require('vinyl'); -var sm = require('source-map'); -var assign = require('object-assign'); -var path = require('path'); +var ts = require("./typescript/typescriptServices"); +var lazy = require("lazy.js"); +var event_stream_1 = require("event-stream"); +var File = require("vinyl"); +var sm = require("source-map"); +var assign = require("object-assign"); +var path = require("path"); var CollectStepResult; (function (CollectStepResult) { CollectStepResult[CollectStepResult["Yes"] = 0] = "Yes"; @@ -71,7 +71,6 @@ function nls() { function isImportNode(node) { return node.kind === 212 /* ImportDeclaration */ || node.kind === 211 /* ImportEqualsDeclaration */; } -var nls; (function (nls_1) { function fileFrom(file, contents, path) { if (path === void 0) { path = file.path; } diff --git a/build/lib/optimize.js b/build/lib/optimize.js index 43fe511c6a5..91c7658e7de 100644 --- a/build/lib/optimize.js +++ b/build/lib/optimize.js @@ -3,21 +3,21 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ 'use strict'; -var path = require('path'); -var gulp = require('gulp'); -var sourcemaps = require('gulp-sourcemaps'); -var filter = require('gulp-filter'); -var minifyCSS = require('gulp-cssnano'); -var uglify = require('gulp-uglify'); -var es = require('event-stream'); -var concat = require('gulp-concat'); -var VinylFile = require('vinyl'); -var bundle = require('./bundle'); -var util = require('./util'); -var i18n = require('./i18n'); -var gulpUtil = require('gulp-util'); -var flatmap = require('gulp-flatmap'); -var pump = require('pump'); +var path = require("path"); +var gulp = require("gulp"); +var sourcemaps = require("gulp-sourcemaps"); +var filter = require("gulp-filter"); +var minifyCSS = require("gulp-cssnano"); +var uglify = require("gulp-uglify"); +var es = require("event-stream"); +var concat = require("gulp-concat"); +var VinylFile = require("vinyl"); +var bundle = require("./bundle"); +var util = require("./util"); +var i18n = require("./i18n"); +var gulpUtil = require("gulp-util"); +var flatmap = require("gulp-flatmap"); +var pump = require("pump"); var REPO_ROOT_PATH = path.join(__dirname, '../..'); function log(prefix, message) { gulpUtil.log(gulpUtil.colors.cyan('[' + prefix + ']'), message); @@ -208,7 +208,7 @@ function uglifyWithCopyrights() { return es.duplex(input, output); } function minifyTask(src, sourceMapBaseUrl) { - var sourceMappingURL = sourceMapBaseUrl && (function (f) { return (sourceMapBaseUrl + "/" + f.relative + ".map"); }); + var sourceMappingURL = sourceMapBaseUrl && (function (f) { return sourceMapBaseUrl + "/" + f.relative + ".map"; }); return function (cb) { var jsFilter = filter('**/*.js', { restore: true }); var cssFilter = filter('**/*.css', { restore: true }); diff --git a/build/lib/reporter.js b/build/lib/reporter.js index 1efc03b9b57..11f360b551a 100644 --- a/build/lib/reporter.js +++ b/build/lib/reporter.js @@ -3,9 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ 'use strict'; -var es = require('event-stream'); -var _ = require('underscore'); -var util = require('gulp-util'); +var es = require("event-stream"); +var _ = require("underscore"); +var util = require("gulp-util"); var allErrors = []; var startTime = null; var count = 0; diff --git a/build/lib/tslint/duplicateImportsRule.js b/build/lib/tslint/duplicateImportsRule.js index d2563ed6fce..a2d38d6c5c4 100644 --- a/build/lib/tslint/duplicateImportsRule.js +++ b/build/lib/tslint/duplicateImportsRule.js @@ -8,12 +8,12 @@ var __extends = (this && this.__extends) || function (d, b) { function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; -var path_1 = require('path'); -var Lint = require('tslint/lib/lint'); +var path_1 = require("path"); +var Lint = require("tslint"); var Rule = (function (_super) { __extends(Rule, _super); function Rule() { - _super.apply(this, arguments); + return _super.apply(this, arguments) || this; } Rule.prototype.apply = function (sourceFile) { return this.applyWithWalker(new ImportPatterns(sourceFile, this.getOptions())); @@ -24,8 +24,9 @@ exports.Rule = Rule; var ImportPatterns = (function (_super) { __extends(ImportPatterns, _super); function ImportPatterns(file, opts) { - _super.call(this, file, opts); - this.imports = Object.create(null); + var _this = _super.call(this, file, opts) || this; + _this.imports = Object.create(null); + return _this; } ImportPatterns.prototype.visitImportDeclaration = function (node) { var path = node.moduleSpecifier.getText(); diff --git a/build/lib/tslint/duplicateImportsRule.ts b/build/lib/tslint/duplicateImportsRule.ts index 23e71ff510c..c648084be1d 100644 --- a/build/lib/tslint/duplicateImportsRule.ts +++ b/build/lib/tslint/duplicateImportsRule.ts @@ -5,7 +5,7 @@ import * as ts from 'typescript'; import { join, dirname } from 'path'; -import * as Lint from 'tslint/lib/lint'; +import * as Lint from 'tslint'; export class Rule extends Lint.Rules.AbstractRule { public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { diff --git a/build/lib/tslint/importPatternsRule.js b/build/lib/tslint/importPatternsRule.js index 3bffefa12f4..0660f7343bb 100644 --- a/build/lib/tslint/importPatternsRule.js +++ b/build/lib/tslint/importPatternsRule.js @@ -8,12 +8,12 @@ var __extends = (this && this.__extends) || function (d, b) { function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; -var Lint = require('tslint/lib/lint'); -var minimatch = require('minimatch'); +var Lint = require("tslint"); +var minimatch = require("minimatch"); var Rule = (function (_super) { __extends(Rule, _super); function Rule() { - _super.apply(this, arguments); + return _super.apply(this, arguments) || this; } Rule.prototype.apply = function (sourceFile) { var configs = this.getOptions().ruleArguments; @@ -31,8 +31,9 @@ exports.Rule = Rule; var ImportPatterns = (function (_super) { __extends(ImportPatterns, _super); function ImportPatterns(file, opts, _config) { - _super.call(this, file, opts); - this._config = _config; + var _this = _super.call(this, file, opts) || this; + _this._config = _config; + return _this; } ImportPatterns.prototype.visitImportDeclaration = function (node) { var path = node.moduleSpecifier.getText(); diff --git a/build/lib/tslint/importPatternsRule.ts b/build/lib/tslint/importPatternsRule.ts index 544d5d7790d..590a7fc0990 100644 --- a/build/lib/tslint/importPatternsRule.ts +++ b/build/lib/tslint/importPatternsRule.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as ts from 'typescript'; -import * as Lint from 'tslint/lib/lint'; +import * as Lint from 'tslint'; import * as minimatch from 'minimatch'; interface ImportPatternsConfig { diff --git a/build/lib/tslint/layeringRule.js b/build/lib/tslint/layeringRule.js index e759167ddf1..87f90e3c5bb 100644 --- a/build/lib/tslint/layeringRule.js +++ b/build/lib/tslint/layeringRule.js @@ -8,12 +8,12 @@ var __extends = (this && this.__extends) || function (d, b) { function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; -var Lint = require('tslint/lib/lint'); -var path_1 = require('path'); +var Lint = require("tslint"); +var path_1 = require("path"); var Rule = (function (_super) { __extends(Rule, _super); function Rule() { - _super.apply(this, arguments); + return _super.apply(this, arguments) || this; } Rule.prototype.apply = function (sourceFile) { var parts = path_1.dirname(sourceFile.fileName).split(/\\|\//); @@ -44,8 +44,9 @@ exports.Rule = Rule; var LayeringRule = (function (_super) { __extends(LayeringRule, _super); function LayeringRule(file, config, opts) { - _super.call(this, file, opts); - this._config = config; + var _this = _super.call(this, file, opts) || this; + _this._config = config; + return _this; } LayeringRule.prototype.visitImportDeclaration = function (node) { var path = node.moduleSpecifier.getText(); diff --git a/build/lib/tslint/layeringRule.ts b/build/lib/tslint/layeringRule.ts index 7da92237bee..c6e34623b2b 100644 --- a/build/lib/tslint/layeringRule.ts +++ b/build/lib/tslint/layeringRule.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as ts from 'typescript'; -import * as Lint from 'tslint/lib/lint'; +import * as Lint from 'tslint'; import { join, dirname } from 'path'; interface Config { diff --git a/build/lib/tslint/noUnexternalizedStringsRule.js b/build/lib/tslint/noUnexternalizedStringsRule.js index cc2fea027ec..711b80925b2 100644 --- a/build/lib/tslint/noUnexternalizedStringsRule.js +++ b/build/lib/tslint/noUnexternalizedStringsRule.js @@ -8,15 +8,15 @@ var __extends = (this && this.__extends) || function (d, b) { function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; -var ts = require('typescript'); -var Lint = require('tslint/lib/lint'); +var ts = require("typescript"); +var Lint = require("tslint"); /** * Implementation of the no-unexternalized-strings rule. */ var Rule = (function (_super) { __extends(Rule, _super); function Rule() { - _super.apply(this, arguments); + return _super.apply(this, arguments) || this; } Rule.prototype.apply = function (sourceFile) { return this.applyWithWalker(new NoUnexternalizedStringsRuleWalker(sourceFile, this.getOptions())); @@ -36,14 +36,13 @@ function isPropertyAssignment(node) { var NoUnexternalizedStringsRuleWalker = (function (_super) { __extends(NoUnexternalizedStringsRuleWalker, _super); function NoUnexternalizedStringsRuleWalker(file, opts) { - var _this = this; - _super.call(this, file, opts); - this.signatures = Object.create(null); - this.ignores = Object.create(null); - this.messageIndex = undefined; - this.keyIndex = undefined; - this.usedKeys = Object.create(null); - var options = this.getOptions(); + var _this = _super.call(this, file, opts) || this; + _this.signatures = Object.create(null); + _this.ignores = Object.create(null); + _this.messageIndex = undefined; + _this.keyIndex = undefined; + _this.usedKeys = Object.create(null); + var options = _this.getOptions(); var first = options && options.length > 0 ? options[0] : null; if (first) { if (Array.isArray(first.signatures)) { @@ -53,12 +52,13 @@ var NoUnexternalizedStringsRuleWalker = (function (_super) { first.ignores.forEach(function (ignore) { return _this.ignores[ignore] = true; }); } if (typeof first.messageIndex !== 'undefined') { - this.messageIndex = first.messageIndex; + _this.messageIndex = first.messageIndex; } if (typeof first.keyIndex !== 'undefined') { - this.keyIndex = first.keyIndex; + _this.keyIndex = first.keyIndex; } } + return _this; } NoUnexternalizedStringsRuleWalker.prototype.visitSourceFile = function (node) { var _this = this; @@ -89,7 +89,6 @@ var NoUnexternalizedStringsRuleWalker = (function (_super) { if (functionName && this.ignores[functionName]) { return; } - var x = "foo"; if (doubleQuoted && (!callInfo || callInfo.argIndex === -1 || !this.signatures[functionName])) { var s = node.getText(); var replacement = new Lint.Replacement(node.getStart(), node.getWidth(), "nls.localize('KEY-" + s.substring(1, s.length - 1) + "', " + s + ")"); @@ -113,8 +112,8 @@ var NoUnexternalizedStringsRuleWalker = (function (_super) { for (var i = 0; i < keyArg.properties.length; i++) { var property = keyArg.properties[i]; if (isPropertyAssignment(property)) { - var name = property.name.getText(); - if (name === 'key') { + var name_1 = property.name.getText(); + if (name_1 === 'key') { var initializer = property.initializer; if (isStringLiteral(initializer)) { this.recordKey(initializer, this.messageIndex ? callInfo.callExpression.arguments[this.messageIndex] : undefined); @@ -167,6 +166,6 @@ var NoUnexternalizedStringsRuleWalker = (function (_super) { node = parent; } }; - NoUnexternalizedStringsRuleWalker.DOUBLE_QUOTE = '"'; return NoUnexternalizedStringsRuleWalker; }(Lint.RuleWalker)); +NoUnexternalizedStringsRuleWalker.DOUBLE_QUOTE = '"'; diff --git a/build/lib/tslint/noUnexternalizedStringsRule.ts b/build/lib/tslint/noUnexternalizedStringsRule.ts index 40cc8ca1e6a..e99adae62d9 100644 --- a/build/lib/tslint/noUnexternalizedStringsRule.ts +++ b/build/lib/tslint/noUnexternalizedStringsRule.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as ts from 'typescript'; -import * as Lint from 'tslint/lib/lint'; +import * as Lint from 'tslint'; /** * Implementation of the no-unexternalized-strings rule. diff --git a/build/lib/util.js b/build/lib/util.js index 92dc53cda6c..d25f7961ac3 100644 --- a/build/lib/util.js +++ b/build/lib/util.js @@ -3,16 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ 'use strict'; -var es = require('event-stream'); -var debounce = require('debounce'); -var _filter = require('gulp-filter'); -var rename = require('gulp-rename'); -var _ = require('underscore'); -var path = require('path'); -var fs = require('fs'); -var _rimraf = require('rimraf'); -var git = require('./git'); -var VinylFile = require('vinyl'); +var es = require("event-stream"); +var debounce = require("debounce"); +var _filter = require("gulp-filter"); +var rename = require("gulp-rename"); +var _ = require("underscore"); +var path = require("path"); +var fs = require("fs"); +var _rimraf = require("rimraf"); +var git = require("./git"); +var VinylFile = require("vinyl"); var NoCancellationToken = { isCancellationRequested: function () { return false; } }; function incremental(streamProvider, initial, supportsCancellation) { var input = es.through(); diff --git a/build/monaco/api.js b/build/monaco/api.js index 469b19bd79d..393aa8b28db 100644 --- a/build/monaco/api.js +++ b/build/monaco/api.js @@ -3,9 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; -var fs = require('fs'); -var ts = require('typescript'); -var path = require('path'); +var fs = require("fs"); +var ts = require("typescript"); +var path = require("path"); var util = require('gulp-util'); function log(message) { var rest = []; diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index e138e426733..671e5031fe0 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -432,7 +432,7 @@ "xterm": { "version": "2.2.3", "from": "git+https://github.com/Tyriar/xterm.js.git#vscode-release/1.9", - "resolved": "git+https://github.com/Tyriar/xterm.js.git#67eaa7ee4a7a028acc6ef88b01e800c24d534875" + "resolved": "git+https://github.com/Tyriar/xterm.js.git#8fb0947bf7d2e506b16b5425c71726c25a64475b" }, "yauzl": { "version": "2.3.1", diff --git a/package.json b/package.json index f3f4fb2a155..208cbfa69b6 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,7 @@ "gulp-shell": "^0.5.2", "gulp-sourcemaps": "^1.6.0", "gulp-tsb": "^2.0.3", - "gulp-tslint": "^4.3.0", + "gulp-tslint": "^7.0.1", "gulp-uglify": "^2.0.0", "gulp-util": "^3.0.6", "gulp-vinyl-zip": "^1.2.2", @@ -90,7 +90,7 @@ "rimraf": "^2.2.8", "sinon": "^1.17.2", "source-map": "^0.4.4", - "tslint": "^3.3.0", + "tslint": "^4.3.1", "typescript": "^2.1.4", "typescript-formatter": "4.0.1", "uglify-js": "2.4.8", diff --git a/src/vs/editor/browser/standalone/standaloneEditor.ts b/src/vs/editor/browser/standalone/standaloneEditor.ts index 64eaab40df2..713398c9dd5 100644 --- a/src/vs/editor/browser/standalone/standaloneEditor.ts +++ b/src/vs/editor/browser/standalone/standaloneEditor.ts @@ -344,6 +344,7 @@ export function createMonacoEditorAPI(): typeof monaco.editor { BareFontInfo: BareFontInfo, FontInfo: FontInfo, TextModelResolvedOptions: editorCommon.TextModelResolvedOptions, + FindMatch: editorCommon.FindMatch, // vars EditorType: editorCommon.EditorType, diff --git a/src/vs/editor/common/commonCodeEditor.ts b/src/vs/editor/common/commonCodeEditor.ts index 02ee9023254..c67207a4b8f 100644 --- a/src/vs/editor/common/commonCodeEditor.ts +++ b/src/vs/editor/common/commonCodeEditor.ts @@ -51,6 +51,8 @@ export abstract class CommonCodeEditor extends EventEmitter implements editorCom public readonly onDidFocusEditor: Event = fromEventEmitter(this, editorCommon.EventType.EditorFocus); public readonly onDidBlurEditor: Event = fromEventEmitter(this, editorCommon.EventType.EditorBlur); public readonly onDidDispose: Event = fromEventEmitter(this, editorCommon.EventType.Disposed); + public readonly onWillType: Event = fromEventEmitter(this, editorCommon.EventType.WillType); + public readonly onDidType: Event = fromEventEmitter(this, editorCommon.EventType.DidType); protected domElement: IContextKeyServiceTarget; @@ -569,6 +571,23 @@ export abstract class CommonCodeEditor extends EventEmitter implements editorCom public trigger(source: string, handlerId: string, payload: any): void { payload = payload || {}; + + // Special case for typing + if (handlerId === editorCommon.Handler.Type) { + if (!this.cursor || typeof payload.text !== 'string' || payload.text.length === 0) { + // nothing to do + return; + } + if (source === 'keyboard') { + this.emit(editorCommon.EventType.WillType, payload.text); + } + this.cursor.trigger(source, handlerId, payload); + if (source === 'keyboard') { + this.emit(editorCommon.EventType.DidType, payload.text); + } + return; + } + let candidate = this.getAction(handlerId); if (candidate !== null) { TPromise.as(candidate.run()).done(null, onUnexpectedError); @@ -712,24 +731,6 @@ export abstract class CommonCodeEditor extends EventEmitter implements editorCom } } - public addTypingListener(character: string, callback: () => void): IDisposable { - if (!this.cursor) { - return { - dispose: () => { - // no-op - } - }; - } - this.cursor.addTypingListener(character, callback); - return { - dispose: () => { - if (this.cursor) { - this.cursor.removeTypingListener(character, callback); - } - } - }; - } - public getLayoutInfo(): editorCommon.EditorLayoutInfo { return this._configuration.editor.layoutInfo; } diff --git a/src/vs/editor/common/controller/cursor.ts b/src/vs/editor/common/controller/cursor.ts index 15e9141dfb0..5d75593dbc9 100644 --- a/src/vs/editor/common/controller/cursor.ts +++ b/src/vs/editor/common/controller/cursor.ts @@ -75,17 +75,11 @@ export class Cursor extends EventEmitter { private modelUnbinds: IDisposable[]; - // Typing listeners - private typingListeners: { - [character: string]: ITypingListener[]; - }; - private cursors: CursorCollection; private cursorUndoStack: ICursorCollectionState[]; private viewModelHelper: IViewModelHelper; private _isHandling: boolean; - private charactersTyped: string; private enableEmptySelectionClipboard: boolean; @@ -108,8 +102,6 @@ export class Cursor extends EventEmitter { this.cursors = new CursorCollection(this.editorId, this.model, this.configuration, this.viewModelHelper); this.cursorUndoStack = []; - this.typingListeners = {}; - this._isHandling = false; this.modelUnbinds = []; @@ -205,25 +197,6 @@ export class Cursor extends EventEmitter { }, 'restoreState', null); } - public addTypingListener(character: string, callback: ITypingListener): void { - if (!this.typingListeners.hasOwnProperty(character)) { - this.typingListeners[character] = []; - } - this.typingListeners[character].push(callback); - } - - public removeTypingListener(character: string, callback: ITypingListener): void { - if (this.typingListeners.hasOwnProperty(character)) { - var listeners = this.typingListeners[character]; - for (var i = 0; i < listeners.length; i++) { - if (listeners[i] === callback) { - listeners.splice(i, 1); - return; - } - } - } - } - private _onModelLanguageChanged(): void { // the mode of this model has changed this.cursors.updateMode(); @@ -310,7 +283,6 @@ export class Cursor extends EventEmitter { private _onHandler(command: string, handler: (ctx: IMultipleCursorOperationContext) => boolean, source: string, data: any): boolean { this._isHandling = true; - this.charactersTyped = ''; var handled = false; @@ -346,22 +318,6 @@ export class Cursor extends EventEmitter { this.cursorUndoStack = []; } - // Ping typing listeners after the model emits events & after I emit events - for (var i = 0; i < this.charactersTyped.length; i++) { - var chr = this.charactersTyped.charAt(i); - if (this.typingListeners.hasOwnProperty(chr)) { - var listeners = this.typingListeners[chr].slice(0); - for (var j = 0, lenJ = listeners.length; j < lenJ; j++) { - // Hoping that listeners understand that the view might be in an awkward state - try { - listeners[j](); - } catch (e) { - onUnexpectedError(e); - } - } - } - } - var newSelections = this.cursors.getSelections(); var newViewSelections = this.cursors.getViewSelections(); @@ -1418,8 +1374,6 @@ export class Cursor extends EventEmitter { chr = text.charAt(i); } - this.charactersTyped += chr; - // Here we must interpret each typed character individually, that's why we create a new context ctx.hasExecutedCommands = this._createAndInterpretHandlerCtx(ctx.eventSource, ctx.eventData, (charHandlerCtx: IMultipleCursorOperationContext) => { diff --git a/src/vs/editor/common/core/characterClassifier.ts b/src/vs/editor/common/core/characterClassifier.ts index e508b6a06c8..bdac75390e3 100644 --- a/src/vs/editor/common/core/characterClassifier.ts +++ b/src/vs/editor/common/core/characterClassifier.ts @@ -56,3 +56,25 @@ export class CharacterClassifier { } } } + +const enum Boolean { + False = 0, + True = 1 +} + +export class CharacterSet { + + private _actual: CharacterClassifier; + + constructor() { + this._actual = new CharacterClassifier(Boolean.False); + } + + public add(charCode: number): void { + this._actual.set(charCode, Boolean.True); + } + + public has(charCode: number): boolean { + return (this._actual.get(charCode) === Boolean.True); + } +} diff --git a/src/vs/editor/common/editorCommon.ts b/src/vs/editor/common/editorCommon.ts index 1a5e11b56df..d80f06c6b7e 100644 --- a/src/vs/editor/common/editorCommon.ts +++ b/src/vs/editor/common/editorCommon.ts @@ -1811,6 +1811,21 @@ export interface ITextModel { findPreviousMatch(searchString: string, searchStart: IPosition, isRegex: boolean, matchCase: boolean, wholeWord: boolean): Range; } +export class FindMatch { + _findMatchBrand: void; + + public readonly range: Range; + public readonly matches: string[]; + + /** + * @internal + */ + constructor(range: Range, matches: string[]) { + this.range = range; + this.matches = matches; + } +} + export interface IReadOnlyModel extends ITextModel { /** * Gets the resource associated with this editor model. @@ -3764,6 +3779,20 @@ export interface ICommonCodeEditor extends IEditor { */ onDidBlurEditor(listener: () => void): IDisposable; + /** + * An event emitted before interpreting typed characters (on the keyboard). + * @event + * @internal + */ + onWillType(listener: (text: string) => void): IDisposable; + + /** + * An event emitted before interpreting typed characters (on the keyboard). + * @event + * @internal + */ + onDidType(listener: (text: string) => void): IDisposable; + /** * Returns true if this editor or one of its widgets has keyboard focus. */ @@ -3905,16 +3934,6 @@ export interface ICommonCodeEditor extends IEditor { * Get the layout info for the editor. */ getLayoutInfo(): EditorLayoutInfo; - - /** - * This listener is notified when a keypress produces a visible character. - * The callback should not do operations on the view, as the view might not be updated to reflect previous typed characters. - * @param character Character to listen to. - * @param callback Function to call when `character` is typed. - * @internal - */ - addTypingListener(character: string, callback: () => void): IDisposable; - } export interface ICommonDiffEditor extends IEditor { @@ -4069,6 +4088,9 @@ export var EventType = { KeyDown: 'keydown', KeyUp: 'keyup', + WillType: 'willType', + DidType: 'didType', + EditorLayout: 'editorLayout', DiffUpdated: 'diffUpdated' diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index 5ca0bd88edb..17aaaff0633 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -14,7 +14,7 @@ import { guessIndentation } from 'vs/editor/common/model/indentationGuesser'; import { DEFAULT_INDENTATION, DEFAULT_TRIM_AUTO_WHITESPACE } from 'vs/editor/common/config/defaultConfig'; import { PrefixSumComputer } from 'vs/editor/common/viewModel/prefixSumComputer'; import { IndentRange, computeRanges } from 'vs/editor/common/model/indentRanges'; -import { CharCode } from 'vs/base/common/charCode'; +import { TextModelSearch, SearchParams } from 'vs/editor/common/model/textModelSearch'; const LIMIT_FIND_COUNT = 999; export const LONG_LINE_BOUNDARY = 1000; @@ -832,67 +832,8 @@ export class TextModel extends OrderGuaranteeEventEmitter implements editorCommo throw new Error('Unknown EOL preference'); } - private static _isMultilineRegexSource(searchString: string): boolean { - if (!searchString || searchString.length === 0) { - return false; - } - - for (let i = 0, len = searchString.length; i < len; i++) { - let chCode = searchString.charCodeAt(i); - - if (chCode === CharCode.Backslash) { - - // move to next char - i++; - - if (i >= len) { - // string ends with a \ - break; - } - - let nextChCode = searchString.charCodeAt(i); - if (nextChCode === CharCode.n || nextChCode === CharCode.r) { - return true; - } - } - } - - return false; - } - - public static parseSearchRequest(searchString: string, isRegex: boolean, matchCase: boolean, wholeWord: boolean): RegExp { - if (searchString === '') { - return null; - } - - // Try to create a RegExp out of the params - let multiline: boolean; - if (isRegex) { - multiline = TextModel._isMultilineRegexSource(searchString); - } else { - multiline = (searchString.indexOf('\n') >= 0); - } - - let regex: RegExp = null; - try { - regex = strings.createRegExp(searchString, isRegex, { matchCase, wholeWord, multiline, global: true }); - } catch (err) { - return null; - } - - if (!regex) { - return null; - } - - return regex; - } - public findMatches(searchString: string, rawSearchScope: any, isRegex: boolean, matchCase: boolean, wholeWord: boolean, limitResultCount: number = LIMIT_FIND_COUNT): Range[] { this._assertNotDisposed(); - let regex = TextModel.parseSearchRequest(searchString, isRegex, matchCase, wholeWord); - if (!regex) { - return []; - } let searchRange: Range; if (Range.isIRange(rawSearchScope)) { @@ -901,239 +842,19 @@ export class TextModel extends OrderGuaranteeEventEmitter implements editorCommo searchRange = this.getFullModelRange(); } - if (regex.multiline) { - return this._doFindMatchesMultiline(searchRange, regex, limitResultCount); - } - return this._doFindMatchesLineByLine(searchRange, regex, limitResultCount); - } - - private _doFindMatchesMultiline(searchRange: Range, searchRegex: RegExp, limitResultCount: number): Range[] { - let deltaOffset = this.getOffsetAt(searchRange.getStartPosition()); - let text = this.getValueInRange(searchRange); - - let result: Range[] = []; - let prevStartOffset = 0; - let prevEndOffset = 0; - let counter = 0; - - let m: RegExpExecArray; - while ((m = searchRegex.exec(text))) { - let startOffset = deltaOffset + m.index; - let endOffset = startOffset + m[0].length; - - if (prevStartOffset === startOffset && prevEndOffset === endOffset) { - // Exit early if the regex matches the same range - return result; - } - - let startPosition = this.getPositionAt(startOffset); - let endPosition = this.getPositionAt(endOffset); - - result[counter++] = new Range(startPosition.lineNumber, startPosition.column, endPosition.lineNumber, endPosition.column); - if (counter >= limitResultCount) { - return result; - } - - prevStartOffset = startOffset; - prevEndOffset = endOffset; - } - - return result; - } - - private _doFindMatchesLineByLine(searchRange: Range, searchRegex: RegExp, limitResultCount: number): Range[] { - let result: Range[] = []; - let text: string; - let counter = 0; - - // Early case for a search range that starts & stops on the same line number - if (searchRange.startLineNumber === searchRange.endLineNumber) { - text = this._lines[searchRange.startLineNumber - 1].text.substring(searchRange.startColumn - 1, searchRange.endColumn - 1); - counter = this._findMatchesInLine(searchRegex, text, searchRange.startLineNumber, searchRange.startColumn - 1, counter, result, limitResultCount); - return result; - } - - // Collect results from first line - text = this._lines[searchRange.startLineNumber - 1].text.substring(searchRange.startColumn - 1); - counter = this._findMatchesInLine(searchRegex, text, searchRange.startLineNumber, searchRange.startColumn - 1, counter, result, limitResultCount); - - // Collect results from middle lines - for (let lineNumber = searchRange.startLineNumber + 1; lineNumber < searchRange.endLineNumber && counter < limitResultCount; lineNumber++) { - counter = this._findMatchesInLine(searchRegex, this._lines[lineNumber - 1].text, lineNumber, 0, counter, result, limitResultCount); - } - - // Collect results from last line - if (counter < limitResultCount) { - text = this._lines[searchRange.endLineNumber - 1].text.substring(0, searchRange.endColumn - 1); - counter = this._findMatchesInLine(searchRegex, text, searchRange.endLineNumber, 0, counter, result, limitResultCount); - } - - return result; + return TextModelSearch.findMatches(this, new SearchParams(searchString, isRegex, matchCase, wholeWord), searchRange, false, limitResultCount).map(e => e.range); } public findNextMatch(searchString: string, rawSearchStart: editorCommon.IPosition, isRegex: boolean, matchCase: boolean, wholeWord: boolean): Range { this._assertNotDisposed(); - let regex = TextModel.parseSearchRequest(searchString, isRegex, matchCase, wholeWord); - if (!regex) { - return null; - } - - let searchStart = this.validatePosition(rawSearchStart); - if (regex.multiline) { - return this._doFindNextMatchMultiline(searchStart, regex); - } - return this._doFindNextMatchLineByLine(searchStart, regex); - - } - - private _doFindNextMatchMultiline(searchStart: Position, searchRegex: RegExp): Range { - let searchTextStart: editorCommon.IPosition = { lineNumber: searchStart.lineNumber, column: 1 }; - let deltaOffset = this.getOffsetAt(searchTextStart); - let text = this.getValueInRange(new Range(searchTextStart.lineNumber, searchTextStart.column, this.getLineCount(), this.getLineMaxColumn(this.getLineCount()))); - searchRegex.lastIndex = searchStart.column - 1; - let m = searchRegex.exec(text); - if (m) { - let startOffset = deltaOffset + m.index; - let endOffset = startOffset + m[0].length; - let startPosition = this.getPositionAt(startOffset); - let endPosition = this.getPositionAt(endOffset); - return new Range(startPosition.lineNumber, startPosition.column, endPosition.lineNumber, endPosition.column); - } - - if (searchStart.lineNumber !== 1 || searchStart.column !== -1) { - // Try again from the top - return this._doFindNextMatchMultiline(new Position(1, 1), searchRegex); - } - - return null; - } - - private _doFindNextMatchLineByLine(searchStart: Position, searchRegex: RegExp): Range { - let lineCount = this.getLineCount(); - let startLineNumber = searchStart.lineNumber; - let text: string; - let r: Range; - - // Look in first line - text = this._lines[startLineNumber - 1].text; - r = this._findFirstMatchInLine(searchRegex, text, startLineNumber, searchStart.column); - if (r) { - return r; - } - - for (let i = 1; i <= lineCount; i++) { - let lineIndex = (startLineNumber + i - 1) % lineCount; - text = this._lines[lineIndex].text; - r = this._findFirstMatchInLine(searchRegex, text, lineIndex + 1, 1); - if (r) { - return r; - } - } - - return null; + let r = TextModelSearch.findNextMatch(this, new SearchParams(searchString, isRegex, matchCase, wholeWord), rawSearchStart, false); + return r ? r.range : null; } public findPreviousMatch(searchString: string, rawSearchStart: editorCommon.IPosition, isRegex: boolean, matchCase: boolean, wholeWord: boolean): Range { this._assertNotDisposed(); - let regex = TextModel.parseSearchRequest(searchString, isRegex, matchCase, wholeWord); - if (!regex) { - return null; - } - - let searchStart = this.validatePosition(rawSearchStart); - if (regex.multiline) { - return this._doFindPreviousMatchMultiline(searchStart, regex); - } - return this._doFindPreviousMatchLineByLine(searchStart, regex); - } - - private _doFindPreviousMatchMultiline(searchStart: Position, searchRegex: RegExp): Range { - let matches = this._doFindMatchesMultiline(new Range(1, 1, searchStart.lineNumber, searchStart.column), searchRegex, 10 * LIMIT_FIND_COUNT); - if (matches.length > 0) { - return matches[matches.length - 1]; - } - - if (searchStart.lineNumber !== this.getLineCount() || searchStart.column !== this.getLineMaxColumn(this.getLineCount())) { - // Try again with all content - return this._doFindPreviousMatchMultiline(new Position(this.getLineCount(), this.getLineMaxColumn(this.getLineCount())), searchRegex); - } - - return null; - } - - private _doFindPreviousMatchLineByLine(searchStart: Position, searchRegex: RegExp): Range { - let lineCount = this.getLineCount(); - let startLineNumber = searchStart.lineNumber; - let text: string; - let r: Range; - - // Look in first line - text = this._lines[startLineNumber - 1].text.substring(0, searchStart.column - 1); - r = this._findLastMatchInLine(searchRegex, text, startLineNumber); - if (r) { - return r; - } - - for (var i = 1; i <= lineCount; i++) { - var lineIndex = (lineCount + startLineNumber - i - 1) % lineCount; - text = this._lines[lineIndex].text; - r = this._findLastMatchInLine(searchRegex, text, lineIndex + 1); - if (r) { - return r; - } - } - - return null; - } - - private _findFirstMatchInLine(searchRegex: RegExp, text: string, lineNumber: number, fromColumn: number): Range { - // Set regex to search from column - searchRegex.lastIndex = fromColumn - 1; - var m: RegExpExecArray = searchRegex.exec(text); - return m ? new Range(lineNumber, m.index + 1, lineNumber, m.index + 1 + m[0].length) : null; - } - - private _findLastMatchInLine(searchRegex: RegExp, text: string, lineNumber: number): Range { - let bestResult: Range = null; - let m: RegExpExecArray; - while ((m = searchRegex.exec(text))) { - let result = new Range(lineNumber, m.index + 1, lineNumber, m.index + 1 + m[0].length); - if (result.equalsRange(bestResult)) { - break; - } - bestResult = result; - if (m.index + m[0].length === text.length) { - // Reached the end of the line - break; - } - } - return bestResult; - } - - private _findMatchesInLine(searchRegex: RegExp, text: string, lineNumber: number, deltaOffset: number, counter: number, result: Range[], limitResultCount: number): number { - var m: RegExpExecArray; - // Reset regex to search from the beginning - searchRegex.lastIndex = 0; - do { - m = searchRegex.exec(text); - if (m) { - var range = new Range(lineNumber, m.index + 1 + deltaOffset, lineNumber, m.index + 1 + m[0].length + deltaOffset); - if (range.equalsRange(result[result.length - 1])) { - // Exit early if the regex matches the same range - return counter; - } - result.push(range); - counter++; - if (counter >= limitResultCount) { - return counter; - } - if (m.index + m[0].length === text.length) { - // Reached the end of the line - return counter; - } - } - } while (m); - return counter; + let r = TextModelSearch.findPreviousMatch(this, new SearchParams(searchString, isRegex, matchCase, wholeWord), rawSearchStart, false); + return r ? r.range : null; } } diff --git a/src/vs/editor/common/model/textModelSearch.ts b/src/vs/editor/common/model/textModelSearch.ts new file mode 100644 index 00000000000..bc516faa852 --- /dev/null +++ b/src/vs/editor/common/model/textModelSearch.ts @@ -0,0 +1,352 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { Position } from 'vs/editor/common/core/position'; +import { Range } from 'vs/editor/common/core/range'; +import { IPosition, FindMatch } from 'vs/editor/common/editorCommon'; +import { CharCode } from 'vs/base/common/charCode'; +import { TextModel } from 'vs/editor/common/model/textModel'; + +const LIMIT_FIND_COUNT = 999; + +export class SearchParams { + public readonly searchString: string; + public readonly isRegex: boolean; + public readonly matchCase: boolean; + public readonly wholeWord: boolean; + + constructor(searchString: string, isRegex: boolean, matchCase: boolean, wholeWord: boolean) { + this.searchString = searchString; + this.isRegex = isRegex; + this.matchCase = matchCase; + this.wholeWord = wholeWord; + } + + private static _isMultilineRegexSource(searchString: string): boolean { + if (!searchString || searchString.length === 0) { + return false; + } + + for (let i = 0, len = searchString.length; i < len; i++) { + const chCode = searchString.charCodeAt(i); + + if (chCode === CharCode.Backslash) { + + // move to next char + i++; + + if (i >= len) { + // string ends with a \ + break; + } + + const nextChCode = searchString.charCodeAt(i); + if (nextChCode === CharCode.n || nextChCode === CharCode.r) { + return true; + } + } + } + + return false; + } + + public parseSearchRequest(): RegExp { + if (this.searchString === '') { + return null; + } + + // Try to create a RegExp out of the params + let multiline: boolean; + if (this.isRegex) { + multiline = SearchParams._isMultilineRegexSource(this.searchString); + } else { + multiline = (this.searchString.indexOf('\n') >= 0); + } + + let regex: RegExp = null; + try { + regex = strings.createRegExp(this.searchString, this.isRegex, { + matchCase: this.matchCase, + wholeWord: this.wholeWord, + multiline, + global: true + }); + } catch (err) { + return null; + } + + if (!regex) { + return null; + } + + return regex; + } +} + +function createFindMatch(range: Range, rawMatches: RegExpExecArray, captureMatches: boolean): FindMatch { + if (!captureMatches) { + return new FindMatch(range, null); + } + let matches: string[] = []; + for (let i = 0, len = rawMatches.length; i < len; i++) { + matches[i] = rawMatches[i]; + } + return new FindMatch(range, matches); +} + +export class TextModelSearch { + + public static findMatches(model: TextModel, searchParams: SearchParams, searchRange: Range, captureMatches: boolean, limitResultCount: number): FindMatch[] { + const regex = searchParams.parseSearchRequest(); + if (!regex) { + return []; + } + + if (regex.multiline) { + return this._doFindMatchesMultiline(model, searchRange, regex, captureMatches, limitResultCount); + } + return this._doFindMatchesLineByLine(model, searchRange, regex, captureMatches, limitResultCount); + } + + private static _doFindMatchesMultiline(model: TextModel, searchRange: Range, searchRegex: RegExp, captureMatches: boolean, limitResultCount: number): FindMatch[] { + const deltaOffset = model.getOffsetAt(searchRange.getStartPosition()); + const text = model.getValueInRange(searchRange); + + const result: FindMatch[] = []; + let prevStartOffset = 0; + let prevEndOffset = 0; + let counter = 0; + + let m: RegExpExecArray; + while ((m = searchRegex.exec(text))) { + const startOffset = deltaOffset + m.index; + const endOffset = startOffset + m[0].length; + + if (prevStartOffset === startOffset && prevEndOffset === endOffset) { + // Exit early if the regex matches the same range + return result; + } + + const startPosition = model.getPositionAt(startOffset); + const endPosition = model.getPositionAt(endOffset); + + result[counter++] = createFindMatch( + new Range(startPosition.lineNumber, startPosition.column, endPosition.lineNumber, endPosition.column), + m, + captureMatches + ); + if (counter >= limitResultCount) { + return result; + } + + prevStartOffset = startOffset; + prevEndOffset = endOffset; + } + + return result; + } + + private static _doFindMatchesLineByLine(model: TextModel, searchRange: Range, searchRegex: RegExp, captureMatches: boolean, limitResultCount: number): FindMatch[] { + const result: FindMatch[] = []; + let counter = 0; + + // Early case for a search range that starts & stops on the same line number + if (searchRange.startLineNumber === searchRange.endLineNumber) { + const text = model.getLineContent(searchRange.startLineNumber).substring(searchRange.startColumn - 1, searchRange.endColumn - 1); + counter = this._findMatchesInLine(searchRegex, text, searchRange.startLineNumber, searchRange.startColumn - 1, counter, result, captureMatches, limitResultCount); + return result; + } + + // Collect results from first line + const text = model.getLineContent(searchRange.startLineNumber).substring(searchRange.startColumn - 1); + counter = this._findMatchesInLine(searchRegex, text, searchRange.startLineNumber, searchRange.startColumn - 1, counter, result, captureMatches, limitResultCount); + + // Collect results from middle lines + for (let lineNumber = searchRange.startLineNumber + 1; lineNumber < searchRange.endLineNumber && counter < limitResultCount; lineNumber++) { + counter = this._findMatchesInLine(searchRegex, model.getLineContent(lineNumber), lineNumber, 0, counter, result, captureMatches, limitResultCount); + } + + // Collect results from last line + if (counter < limitResultCount) { + const text = model.getLineContent(searchRange.endLineNumber).substring(0, searchRange.endColumn - 1); + counter = this._findMatchesInLine(searchRegex, text, searchRange.endLineNumber, 0, counter, result, captureMatches, limitResultCount); + } + + return result; + } + + private static _findMatchesInLine(searchRegex: RegExp, text: string, lineNumber: number, deltaOffset: number, counter: number, result: FindMatch[], captureMatches: boolean, limitResultCount: number): number { + let m: RegExpExecArray; + // Reset regex to search from the beginning + searchRegex.lastIndex = 0; + do { + m = searchRegex.exec(text); + if (m) { + const range = new Range(lineNumber, m.index + 1 + deltaOffset, lineNumber, m.index + 1 + m[0].length + deltaOffset); + if (result.length > 0 && range.equalsRange(result[result.length - 1].range)) { + // Exit early if the regex matches the same range + return counter; + } + result.push(createFindMatch(range, m, captureMatches)); + counter++; + if (counter >= limitResultCount) { + return counter; + } + if (m.index + m[0].length === text.length) { + // Reached the end of the line + return counter; + } + } + } while (m); + return counter; + } + + public static findNextMatch(model: TextModel, searchParams: SearchParams, rawSearchStart: IPosition, captureMatches: boolean): FindMatch { + const regex = searchParams.parseSearchRequest(); + if (!regex) { + return null; + } + + const searchStart = model.validatePosition(rawSearchStart); + if (regex.multiline) { + return this._doFindNextMatchMultiline(model, searchStart, regex, captureMatches); + } + return this._doFindNextMatchLineByLine(model, searchStart, regex, captureMatches); + } + + private static _doFindNextMatchMultiline(model: TextModel, searchStart: Position, searchRegex: RegExp, captureMatches: boolean): FindMatch { + const searchTextStart: IPosition = { lineNumber: searchStart.lineNumber, column: 1 }; + const deltaOffset = model.getOffsetAt(searchTextStart); + const lineCount = model.getLineCount(); + const text = model.getValueInRange(new Range(searchTextStart.lineNumber, searchTextStart.column, lineCount, model.getLineMaxColumn(lineCount))); + searchRegex.lastIndex = searchStart.column - 1; + let m = searchRegex.exec(text); + if (m) { + const startOffset = deltaOffset + m.index; + const endOffset = startOffset + m[0].length; + const startPosition = model.getPositionAt(startOffset); + const endPosition = model.getPositionAt(endOffset); + return createFindMatch( + new Range(startPosition.lineNumber, startPosition.column, endPosition.lineNumber, endPosition.column), + m, + captureMatches + ); + } + + if (searchStart.lineNumber !== 1 || searchStart.column !== -1) { + // Try again from the top + return this._doFindNextMatchMultiline(model, new Position(1, 1), searchRegex, captureMatches); + } + + return null; + } + + private static _doFindNextMatchLineByLine(model: TextModel, searchStart: Position, searchRegex: RegExp, captureMatches: boolean): FindMatch { + const lineCount = model.getLineCount(); + const startLineNumber = searchStart.lineNumber; + + // Look in first line + const text = model.getLineContent(startLineNumber); + const r = this._findFirstMatchInLine(searchRegex, text, startLineNumber, searchStart.column, captureMatches); + if (r) { + return r; + } + + for (let i = 1; i <= lineCount; i++) { + const lineIndex = (startLineNumber + i - 1) % lineCount; + const text = model.getLineContent(lineIndex + 1); + const r = this._findFirstMatchInLine(searchRegex, text, lineIndex + 1, 1, captureMatches); + if (r) { + return r; + } + } + + return null; + } + + private static _findFirstMatchInLine(searchRegex: RegExp, text: string, lineNumber: number, fromColumn: number, captureMatches: boolean): FindMatch { + // Set regex to search from column + searchRegex.lastIndex = fromColumn - 1; + const m: RegExpExecArray = searchRegex.exec(text); + if (m) { + return createFindMatch( + new Range(lineNumber, m.index + 1, lineNumber, m.index + 1 + m[0].length), + m, + captureMatches + ); + } + return null; + } + + public static findPreviousMatch(model: TextModel, searchParams: SearchParams, rawSearchStart: IPosition, captureMatches: boolean): FindMatch { + const regex = searchParams.parseSearchRequest(); + if (!regex) { + return null; + } + + const searchStart = model.validatePosition(rawSearchStart); + if (regex.multiline) { + return this._doFindPreviousMatchMultiline(model, searchStart, regex, captureMatches); + } + return this._doFindPreviousMatchLineByLine(model, searchStart, regex, captureMatches); + } + + private static _doFindPreviousMatchMultiline(model: TextModel, searchStart: Position, searchRegex: RegExp, captureMatches: boolean): FindMatch { + const matches = this._doFindMatchesMultiline(model, new Range(1, 1, searchStart.lineNumber, searchStart.column), searchRegex, captureMatches, 10 * LIMIT_FIND_COUNT); + if (matches.length > 0) { + return matches[matches.length - 1]; + } + + const lineCount = model.getLineCount(); + if (searchStart.lineNumber !== lineCount || searchStart.column !== model.getLineMaxColumn(lineCount)) { + // Try again with all content + return this._doFindPreviousMatchMultiline(model, new Position(lineCount, model.getLineMaxColumn(lineCount)), searchRegex, captureMatches); + } + + return null; + } + + private static _doFindPreviousMatchLineByLine(model: TextModel, searchStart: Position, searchRegex: RegExp, captureMatches: boolean): FindMatch { + const lineCount = model.getLineCount(); + const startLineNumber = searchStart.lineNumber; + + // Look in first line + const text = model.getLineContent(startLineNumber).substring(0, searchStart.column - 1); + const r = this._findLastMatchInLine(searchRegex, text, startLineNumber, captureMatches); + if (r) { + return r; + } + + for (let i = 1; i <= lineCount; i++) { + const lineIndex = (lineCount + startLineNumber - i - 1) % lineCount; + const text = model.getLineContent(lineIndex + 1); + const r = this._findLastMatchInLine(searchRegex, text, lineIndex + 1, captureMatches); + if (r) { + return r; + } + } + + return null; + } + + private static _findLastMatchInLine(searchRegex: RegExp, text: string, lineNumber: number, captureMatches: boolean): FindMatch { + let bestResult: FindMatch = null; + let m: RegExpExecArray; + while ((m = searchRegex.exec(text))) { + const result = new Range(lineNumber, m.index + 1, lineNumber, m.index + 1 + m[0].length); + if (bestResult && result.equalsRange(bestResult.range)) { + break; + } + bestResult = createFindMatch(result, m, captureMatches); + if (m.index + m[0].length === text.length) { + // Reached the end of the line + break; + } + } + return bestResult; + } +} diff --git a/src/vs/editor/common/modes.ts b/src/vs/editor/common/modes.ts index 62e5bb8d593..9e4f4bbf394 100644 --- a/src/vs/editor/common/modes.ts +++ b/src/vs/editor/common/modes.ts @@ -214,6 +214,7 @@ export interface ISuggestion { filterText?: string; sortText?: string; noAutoAccept?: boolean; + commitCharacters?: string[]; overwriteBefore?: number; overwriteAfter?: number; additionalTextEdits?: editorCommon.ISingleEditOperation[]; diff --git a/src/vs/editor/contrib/find/common/findModel.ts b/src/vs/editor/contrib/find/common/findModel.ts index 83c3bde8e72..3f16d1c9fb6 100644 --- a/src/vs/editor/contrib/find/common/findModel.ts +++ b/src/vs/editor/contrib/find/common/findModel.ts @@ -11,13 +11,13 @@ import { ReplaceCommand } from 'vs/editor/common/commands/replaceCommand'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import * as editorCommon from 'vs/editor/common/editorCommon'; -import { TextModel } from 'vs/editor/common/model/textModel'; import { FindDecorations } from './findDecorations'; import { FindReplaceState, FindReplaceStateChangedEvent } from './findState'; import { ReplaceAllCommand } from './replaceAllCommand'; import { Selection } from 'vs/editor/common/core/selection'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { IKeybindings } from 'vs/platform/keybinding/common/keybinding'; +import { SearchParams } from 'vs/editor/common/model/textModelSearch'; export const ToggleCaseSensitiveKeybinding: IKeybindings = { primary: KeyMod.Alt | KeyCode.KEY_C, @@ -266,12 +266,8 @@ export class FindModelBoundToEditorModel { this._moveToPrevMatch(this._editor.getSelection().getStartPosition()); } - private _moveToNextMatch(nextMatch: Range): void - private _moveToNextMatch(after: Position): void - private _moveToNextMatch(arg: any): void { - // @sandeep TS(2.0.2) - Adding cast to keep semantic. Necessary since the test are for interface but the code expects - // implemations. - let nextMatch = Range.isIRange(arg) ? arg : Position.isIPosition(arg) ? this._getNextMatch(arg as Position) : null; + private _moveToNextMatch(after: Position): void { + let nextMatch = this._getNextMatch(after); if (nextMatch) { this._setCurrentFindMatch(nextMatch as Range); } @@ -327,7 +323,7 @@ export class FindModelBoundToEditorModel { if (!nextMatch) { // there is precisely one match and selection is on top of it - return; + return null; } if (!isRecursed && !searchRange.containsRange(nextMatch)) { @@ -343,7 +339,8 @@ export class FindModelBoundToEditorModel { private getReplaceString(matchRange: Range): string { if (this._state.isRegex) { - let regExp = TextModel.parseSearchRequest(this._state.searchString, this._state.isRegex, this._state.matchCase, this._state.wholeWord); + let searchParams = new SearchParams(this._state.searchString, this._state.isRegex, this._state.matchCase, this._state.wholeWord); + let regExp = searchParams.parseSearchRequest(); let replacePattern = new ReplacePattern(this._state.replaceString, true, regExp); let model = this._editor.getModel(); let matchedString = model.getValueInRange(matchRange); @@ -388,7 +385,7 @@ export class FindModelBoundToEditorModel { this.research(true); } else { this._decorations.setStartPosition(this._editor.getPosition()); - this._moveToNextMatch(nextMatch); + this._setCurrentFindMatch(nextMatch); } } } diff --git a/src/vs/editor/contrib/format/common/formatActions.ts b/src/vs/editor/contrib/format/common/formatActions.ts index 8b74b7e3ba2..73737d38288 100644 --- a/src/vs/editor/contrib/format/common/formatActions.ts +++ b/src/vs/editor/contrib/format/common/formatActions.ts @@ -18,6 +18,7 @@ import { EditOperationsCommand } from './formatCommand'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { ICodeEditorService } from 'vs/editor/common/services/codeEditorService'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService'; +import { CharacterSet } from 'vs/editor/common/core/characterClassifier'; import ModeContextKeys = editorCommon.ModeContextKeys; import EditorContextKeys = editorCommon.EditorContextKeys; @@ -68,9 +69,16 @@ class FormatOnType implements editorCommon.IEditorContribution { } // register typing listeners that will trigger the format - support.autoFormatTriggerCharacters.forEach(ch => { - this.callOnModel.push(this.editor.addTypingListener(ch, this.trigger.bind(this, ch))); - }); + let triggerChars = new CharacterSet(); + for (let ch of support.autoFormatTriggerCharacters) { + triggerChars.add(ch.charCodeAt(0)); + } + this.callOnModel.push(this.editor.onDidType((text: string) => { + let lastCharCode = text.charCodeAt(text.length - 1); + if (triggerChars.has(lastCharCode)) { + this.trigger(String.fromCharCode(lastCharCode)); + } + })); } private trigger(ch: string): void { diff --git a/src/vs/editor/contrib/parameterHints/browser/parameterHintsWidget.ts b/src/vs/editor/contrib/parameterHints/browser/parameterHintsWidget.ts index ff24ce99d14..3f7ad0a1b8a 100644 --- a/src/vs/editor/contrib/parameterHints/browser/parameterHintsWidget.ts +++ b/src/vs/editor/contrib/parameterHints/browser/parameterHintsWidget.ts @@ -21,6 +21,7 @@ import { ICommonCodeEditor, ICursorSelectionChangedEvent, IConfigurationChangedE import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { Context, provideSignatureHelp } from '../common/parameterHints'; import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; +import { CharacterSet } from 'vs/editor/common/core/characterClassifier'; const $ = dom.$; @@ -117,22 +118,21 @@ export class ParameterHintsModel extends Disposable { return; } - const allTriggerCharacters: string[] = []; + const triggerChars = new CharacterSet(); for (const support of SignatureHelpProviderRegistry.ordered(model)) { if (Array.isArray(support.signatureHelpTriggerCharacters)) { - allTriggerCharacters.push(...support.signatureHelpTriggerCharacters); + for (const ch of support.signatureHelpTriggerCharacters) { + triggerChars.add(ch.charCodeAt(0)); + } } } - allTriggerCharacters.sort(); - this.triggerCharactersListeners.length = 0; - let lastCh: string; - for (const ch of allTriggerCharacters) { - if (ch !== lastCh) { - lastCh = ch; - this.triggerCharactersListeners.push(this.editor.addTypingListener(ch, () => this.trigger())); + this.triggerCharactersListeners.push(this.editor.onDidType((text: string) => { + let lastCharCode = text.charCodeAt(text.length - 1); + if (triggerChars.has(lastCharCode)) { + this.trigger(); } - } + })); } private onCursorChange(e: ICursorSelectionChangedEvent): void { diff --git a/src/vs/editor/contrib/suggest/browser/suggestController.ts b/src/vs/editor/contrib/suggest/browser/suggestController.ts index 31697b381f3..1b9332f32aa 100644 --- a/src/vs/editor/contrib/suggest/browser/suggestController.ts +++ b/src/vs/editor/contrib/suggest/browser/suggestController.ts @@ -7,6 +7,7 @@ import * as nls from 'vs/nls'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { onUnexpectedError } from 'vs/base/common/errors'; +import { isFalsyOrEmpty } from 'vs/base/common/arrays'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -25,6 +26,47 @@ import { SuggestModel } from '../common/suggestModel'; import { ICompletionItem } from '../common/completionModel'; import { SuggestWidget } from './suggestWidget'; +class AcceptOnCharacterOracle { + + private _disposables: IDisposable[] = []; + + private _activeAcceptCharacters = new Set(); + private _activeItem: ICompletionItem; + + constructor(editor: ICodeEditor, widget: SuggestWidget, accept: (item: ICompletionItem) => any) { + + this._disposables.push(widget.onDidFocus(item => { + if (!item || isFalsyOrEmpty(item.suggestion.commitCharacters)) { + this._activeItem = undefined; + return; + } + + this._activeItem = item; + this._activeAcceptCharacters.clear(); + for (const ch of item.suggestion.commitCharacters) { + this._activeAcceptCharacters.add(ch[0]); + } + })); + + this._disposables.push(editor.onWillType(text => { + if (this._activeItem) { + const ch = text[text.length - 1]; + if (this._activeAcceptCharacters.has(ch)) { + accept(this._activeItem); + } + } + })); + } + + reset(): void { + this._activeItem = undefined; + } + + dispose() { + dispose(this._disposables); + } +} + @editorContribution export class SuggestController implements IEditorContribution { private static ID: string = 'editor.contrib.suggestController'; @@ -59,6 +101,14 @@ export class SuggestController implements IEditorContribution { this.widget = instantiationService.createInstance(SuggestWidget, this.editor); this.toDispose.push(this.widget.onDidSelect(this.onDidSelectItem, this)); + + // Wire up logic to accept a suggestion on certain characters + const autoAcceptOracle = new AcceptOnCharacterOracle(editor, this.widget, item => this.onDidSelectItem(item)); + this.toDispose.push( + this.model.onDidCancel(autoAcceptOracle.reset, autoAcceptOracle), + this.model.onDidTrigger(autoAcceptOracle.reset, autoAcceptOracle), + autoAcceptOracle + ); } getId(): string { diff --git a/src/vs/editor/contrib/suggest/browser/suggestWidget.ts b/src/vs/editor/contrib/suggest/browser/suggestWidget.ts index b9ec550e7eb..6353559e052 100644 --- a/src/vs/editor/contrib/suggest/browser/suggestWidget.ts +++ b/src/vs/editor/contrib/suggest/browser/suggestWidget.ts @@ -293,7 +293,7 @@ export class SuggestWidget implements IContentWidget, IDelegate static NO_SUGGESTIONS_MESSAGE: string = nls.localize('suggestWidget.noSuggestions', "No suggestions."); // Editor.IContentWidget.allowEditorOverflow - allowEditorOverflow = true; + readonly allowEditorOverflow = true; private state: State; private isAuto: boolean; @@ -312,12 +312,16 @@ export class SuggestWidget implements IContentWidget, IDelegate private suggestWidgetMultipleSuggestions: IContextKey; private suggestionSupportsAutoAccept: IContextKey; - private onDidSelectEmitter = new Emitter(); - private editorBlurTimeout: TPromise; private showTimeout: TPromise; private toDispose: IDisposable[]; + private onDidSelectEmitter = new Emitter(); + private onDidFocusEmitter = new Emitter(); + + readonly onDidSelect: Event = this.onDidSelectEmitter.event; + readonly onDidFocus: Event = this.onDidFocusEmitter.event; + constructor( private editor: ICodeEditor, @ITelemetryService private telemetryService: ITelemetryService, @@ -472,6 +476,9 @@ export class SuggestWidget implements IContentWidget, IDelegate }) .then(null, err => !isPromiseCanceledError(err) && onUnexpectedError(err)) .then(() => this.currentSuggestionDetails = null); + + // emit an event + this.onDidFocusEmitter.fire(item); } private setState(state: State): void { @@ -528,10 +535,6 @@ export class SuggestWidget implements IContentWidget, IDelegate } } - get onDidSelect(): Event { - return this.onDidSelectEmitter.event; - } - showTriggered(auto: boolean) { if (this.state !== State.Hidden) { return; diff --git a/src/vs/editor/contrib/suggest/common/suggestModel.ts b/src/vs/editor/contrib/suggest/common/suggestModel.ts index 5a3f163ea12..f9c51e51cf9 100644 --- a/src/vs/editor/contrib/suggest/common/suggestModel.ts +++ b/src/vs/editor/contrib/suggest/common/suggestModel.ts @@ -6,7 +6,6 @@ import { onUnexpectedError } from 'vs/base/common/errors'; import { isFalsyOrEmpty } from 'vs/base/common/arrays'; -import { forEach } from 'vs/base/common/collections'; import Event, { Emitter } from 'vs/base/common/event'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { TPromise } from 'vs/base/common/winjs.base'; @@ -87,7 +86,7 @@ export class SuggestModel implements IDisposable { private toDispose: IDisposable[] = []; private quickSuggestDelay: number; - private triggerCharacterListeners: IDisposable[] = []; + private triggerCharacterListener: IDisposable; private triggerAutoSuggestPromise: TPromise; private state: State; @@ -139,9 +138,8 @@ export class SuggestModel implements IDisposable { } dispose(): void { - dispose([this._onDidCancel, this._onDidSuggest, this._onDidTrigger]); + dispose([this._onDidCancel, this._onDidSuggest, this._onDidTrigger, this.triggerCharacterListener]); this.toDispose = dispose(this.toDispose); - this.triggerCharacterListeners = dispose(this.triggerCharacterListeners); this.cancel(); } @@ -157,7 +155,7 @@ export class SuggestModel implements IDisposable { private updateTriggerCharacters(): void { - this.triggerCharacterListeners = dispose(this.triggerCharacterListeners); + dispose(this.triggerCharacterListener); if (this.editor.getConfiguration().readOnly || !this.editor.getModel() @@ -181,10 +179,12 @@ export class SuggestModel implements IDisposable { } } - forEach(supportsByTriggerCharacter, entry => { - this.triggerCharacterListeners.push(this.editor.addTypingListener(entry.key, () => { - this.trigger(true, false, entry.value); - })); + this.triggerCharacterListener = this.editor.onDidType((text: string) => { + let lastChar = text.charAt(text.length - 1); + let supports = supportsByTriggerCharacter[lastChar]; + if (supports) { + this.trigger(true, false, supports); + } }); } diff --git a/src/vs/editor/test/common/model/model.test.ts b/src/vs/editor/test/common/model/model.test.ts index ef0838928cc..ab43109d1dd 100644 --- a/src/vs/editor/test/common/model/model.test.ts +++ b/src/vs/editor/test/common/model/model.test.ts @@ -13,7 +13,6 @@ import { IModelContentChangedLinesDeletedEvent, IModelContentChangedLinesInsertedEvent } from 'vs/editor/common/editorCommon'; import { Model } from 'vs/editor/common/model/model'; -import { TextModel } from 'vs/editor/common/model/textModel'; // --------- utils @@ -393,440 +392,3 @@ suite('Editor Model - Words', () => { assert.deepEqual(thisModel.getWordAtPosition(new Position(1, 28)), null); }); }); - - -// --------- Find -suite('Editor Model - Find', () => { - - function toArrRange(r: Range): [number, number, number, number] { - return [r.startLineNumber, r.startColumn, r.endLineNumber, r.endColumn]; - } - - function assertFindMatches(text: string, searchString: string, isRegex: boolean, matchCase: boolean, wholeWord: boolean, expected: [number, number, number, number][]): void { - let model = Model.createFromString(text); - - let actualRanges = model.findMatches(searchString, false, isRegex, matchCase, wholeWord); - let actual = actualRanges.map(toArrRange); - - assert.deepEqual(actual, expected, 'findMatches OK'); - - // test `findNextMatch` - let startPos = new Position(1, 1); - let match = model.findNextMatch(searchString, startPos, isRegex, matchCase, wholeWord); - assert.deepEqual(toArrRange(match), expected[0], `findNextMatch ${startPos}`); - for (let i = 0; i < expected.length; i++) { - startPos = new Position(expected[i][0], expected[i][1]); - match = model.findNextMatch(searchString, startPos, isRegex, matchCase, wholeWord); - assert.deepEqual(toArrRange(match), expected[i], `findNextMatch ${startPos}`); - } - - // test `findPrevMatch` - startPos = new Position(model.getLineCount(), model.getLineMaxColumn(model.getLineCount())); - match = model.findPreviousMatch(searchString, startPos, isRegex, matchCase, wholeWord); - assert.deepEqual(toArrRange(match), expected[expected.length - 1], `findPrevMatch ${startPos}`); - for (let i = 0; i < expected.length; i++) { - startPos = new Position(expected[i][2], expected[i][3]); - match = model.findPreviousMatch(searchString, startPos, isRegex, matchCase, wholeWord); - assert.deepEqual(toArrRange(match), expected[i], `findPrevMatch ${startPos}`); - } - - model.dispose(); - } - - let regularText = [ - 'This is some foo - bar text which contains foo and bar - as in Barcelona.', - 'Now it begins a word fooBar and now it is caps Foo-isn\'t this great?', - 'And here\'s a dull line with nothing interesting in it', - 'It is also interesting if it\'s part of a word like amazingFooBar', - 'Again nothing interesting here' - ]; - - test('Simple find', () => { - assertFindMatches( - regularText.join('\n'), - 'foo', false, false, false, - [ - [1, 14, 1, 17], - [1, 44, 1, 47], - [2, 22, 2, 25], - [2, 48, 2, 51], - [4, 59, 4, 62] - ] - ); - }); - - test('Case sensitive find', () => { - assertFindMatches( - regularText.join('\n'), - 'foo', false, true, false, - [ - [1, 14, 1, 17], - [1, 44, 1, 47], - [2, 22, 2, 25] - ] - ); - }); - - test('Whole words find', () => { - assertFindMatches( - regularText.join('\n'), - 'foo', false, false, true, - [ - [1, 14, 1, 17], - [1, 44, 1, 47], - [2, 48, 2, 51] - ] - ); - }); - - test('/^/ find', () => { - assertFindMatches( - regularText.join('\n'), - '^', true, false, false, - [ - [1, 1, 1, 1], - [2, 1, 2, 1], - [3, 1, 3, 1], - [4, 1, 4, 1], - [5, 1, 5, 1] - ] - ); - }); - - test('/$/ find', () => { - assertFindMatches( - regularText.join('\n'), - '$', true, false, false, - [ - [1, 74, 1, 74], - [2, 69, 2, 69], - [3, 54, 3, 54], - [4, 65, 4, 65], - [5, 31, 5, 31] - ] - ); - }); - - test('/.*/ find', () => { - assertFindMatches( - regularText.join('\n'), - '.*', true, false, false, - [ - [1, 1, 1, 74], - [2, 1, 2, 69], - [3, 1, 3, 54], - [4, 1, 4, 65], - [5, 1, 5, 31] - ] - ); - }); - - test('/^$/ find', () => { - assertFindMatches( - [ - 'This is some foo - bar text which contains foo and bar - as in Barcelona.', - '', - 'And here\'s a dull line with nothing interesting in it', - '', - 'Again nothing interesting here' - ].join('\n'), - '^$', true, false, false, - [ - [2, 1, 2, 1], - [4, 1, 4, 1] - ] - ); - }); - - test('multiline find 1', () => { - assertFindMatches( - [ - 'Just some text text', - 'Just some text text', - 'some text again', - 'again some text' - ].join('\n'), - 'text\\n', true, false, false, - [ - [1, 16, 2, 1], - [2, 16, 3, 1], - ] - ); - }); - - test('multiline find 2', () => { - assertFindMatches( - [ - 'Just some text text', - 'Just some text text', - 'some text again', - 'again some text' - ].join('\n'), - 'text\\nJust', true, false, false, - [ - [1, 16, 2, 5] - ] - ); - }); - - test('multiline find 3', () => { - assertFindMatches( - [ - 'Just some text text', - 'Just some text text', - 'some text again', - 'again some text' - ].join('\n'), - '\\nagain', true, false, false, - [ - [3, 16, 4, 6] - ] - ); - }); - - test('multiline find 4', () => { - assertFindMatches( - [ - 'Just some text text', - 'Just some text text', - 'some text again', - 'again some text' - ].join('\n'), - '.*\\nJust.*\\n', true, false, false, - [ - [1, 1, 3, 1] - ] - ); - }); - - test('multiline find with line beginning regex', () => { - assertFindMatches( - [ - 'if', - 'else', - '', - 'if', - 'else' - ].join('\n'), - '^if\\nelse', true, false, false, - [ - [1, 1, 2, 5], - [4, 1, 5, 5] - ] - ); - }); - - test('matching empty lines using boundary expression', () => { - assertFindMatches( - [ - 'if', - '', - 'else', - ' ', - 'if', - ' ', - 'else' - ].join('\n'), - '^\\s*$\\n', true, false, false, - [ - [2, 1, 3, 1], - [4, 1, 5, 1], - [6, 1, 7, 1] - ] - ); - }); - - test('matching lines starting with A and ending with B', () => { - assertFindMatches( - [ - 'a if b', - 'a', - 'ab', - 'eb' - ].join('\n'), - '^a.*b$', true, false, false, - [ - [1, 1, 1, 7], - [3, 1, 3, 3] - ] - ); - }); - - test('multiline find with line ending regex', () => { - assertFindMatches( - [ - 'if', - 'else', - '', - 'if', - 'elseif', - 'else' - ].join('\n'), - 'if\\nelse$', true, false, false, - [ - [1, 1, 2, 5], - [5, 5, 6, 5] - ] - ); - }); - - test('issue #4836 - ^.*$', () => { - assertFindMatches( - [ - 'Just some text text', - '', - 'some text again', - '', - 'again some text' - ].join('\n'), - '^.*$', true, false, false, - [ - [1, 1, 1, 20], - [2, 1, 2, 1], - [3, 1, 3, 16], - [4, 1, 4, 1], - [5, 1, 5, 16], - ] - ); - }); - - test('multiline find for non-regex string', () => { - assertFindMatches( - [ - 'Just some text text', - 'some text text', - 'some text again', - 'again some text', - 'but not some' - ].join('\n'), - 'text\nsome', false, false, false, - [ - [1, 16, 2, 5], - [2, 11, 3, 5], - ] - ); - }); - - test('findNextMatch without regex', () => { - var testObject = new TextModel([], TextModel.toRawText('line line one\nline two\nthree', TextModel.DEFAULT_CREATION_OPTIONS)); - - let actual = testObject.findNextMatch('line', { lineNumber: 1, column: 1 }, false, false, false); - assert.equal(new Range(1, 1, 1, 5).toString(), actual.toString()); - - actual = testObject.findNextMatch('line', actual.getEndPosition(), false, false, false); - assert.equal(new Range(1, 6, 1, 10).toString(), actual.toString()); - - actual = testObject.findNextMatch('line', { lineNumber: 1, column: 3 }, false, false, false); - assert.equal(new Range(1, 6, 1, 10).toString(), actual.toString()); - - actual = testObject.findNextMatch('line', actual.getEndPosition(), false, false, false); - assert.equal(new Range(2, 1, 2, 5).toString(), actual.toString()); - - actual = testObject.findNextMatch('line', actual.getEndPosition(), false, false, false); - assert.equal(new Range(1, 1, 1, 5).toString(), actual.toString()); - - testObject.dispose(); - }); - - test('findNextMatch with beginning boundary regex', () => { - var testObject = new TextModel([], TextModel.toRawText('line one\nline two\nthree', TextModel.DEFAULT_CREATION_OPTIONS)); - - let actual = testObject.findNextMatch('^line', { lineNumber: 1, column: 1 }, true, false, false); - assert.equal(new Range(1, 1, 1, 5).toString(), actual.toString()); - - actual = testObject.findNextMatch('^line', actual.getEndPosition(), true, false, false); - assert.equal(new Range(2, 1, 2, 5).toString(), actual.toString()); - - actual = testObject.findNextMatch('^line', { lineNumber: 1, column: 3 }, true, false, false); - assert.equal(new Range(2, 1, 2, 5).toString(), actual.toString()); - - actual = testObject.findNextMatch('^line', actual.getEndPosition(), true, false, false); - assert.equal(new Range(1, 1, 1, 5).toString(), actual.toString()); - - testObject.dispose(); - }); - - test('findNextMatch with beginning boundary regex and line has repetitive beginnings', () => { - var testObject = new TextModel([], TextModel.toRawText('line line one\nline two\nthree', TextModel.DEFAULT_CREATION_OPTIONS)); - - let actual = testObject.findNextMatch('^line', { lineNumber: 1, column: 1 }, true, false, false); - assert.equal(new Range(1, 1, 1, 5).toString(), actual.toString()); - - actual = testObject.findNextMatch('^line', actual.getEndPosition(), true, false, false); - assert.equal(new Range(2, 1, 2, 5).toString(), actual.toString()); - - actual = testObject.findNextMatch('^line', { lineNumber: 1, column: 3 }, true, false, false); - assert.equal(new Range(2, 1, 2, 5).toString(), actual.toString()); - - actual = testObject.findNextMatch('^line', actual.getEndPosition(), true, false, false); - assert.equal(new Range(1, 1, 1, 5).toString(), actual.toString()); - - testObject.dispose(); - }); - - test('findNextMatch with beginning boundary multiline regex and line has repetitive beginnings', () => { - var testObject = new TextModel([], TextModel.toRawText('line line one\nline two\nline three\nline four', TextModel.DEFAULT_CREATION_OPTIONS)); - - let actual = testObject.findNextMatch('^line.*\\nline', { lineNumber: 1, column: 1 }, true, false, false); - assert.equal(new Range(1, 1, 2, 5).toString(), actual.toString()); - - actual = testObject.findNextMatch('^line.*\\nline', actual.getEndPosition(), true, false, false); - assert.equal(new Range(3, 1, 4, 5).toString(), actual.toString()); - - actual = testObject.findNextMatch('^line.*\\nline', { lineNumber: 2, column: 1 }, true, false, false); - assert.equal(new Range(2, 1, 3, 5).toString(), actual.toString()); - - testObject.dispose(); - }); - - test('findNextMatch with ending boundary regex', () => { - var testObject = new TextModel([], TextModel.toRawText('one line line\ntwo line\nthree', TextModel.DEFAULT_CREATION_OPTIONS)); - - let actual = testObject.findNextMatch('line$', { lineNumber: 1, column: 1 }, true, false, false); - assert.equal(new Range(1, 10, 1, 14).toString(), actual.toString()); - - actual = testObject.findNextMatch('line$', { lineNumber: 1, column: 4 }, true, false, false); - assert.equal(new Range(1, 10, 1, 14).toString(), actual.toString()); - - actual = testObject.findNextMatch('line$', actual.getEndPosition(), true, false, false); - assert.equal(new Range(2, 5, 2, 9).toString(), actual.toString()); - - actual = testObject.findNextMatch('line$', actual.getEndPosition(), true, false, false); - assert.equal(new Range(1, 10, 1, 14).toString(), actual.toString()); - - testObject.dispose(); - }); - - function assertParseSearchResult(searchString: string, isRegex: boolean, matchCase: boolean, wholeWord: boolean, expected: RegExp): void { - let actual = TextModel.parseSearchRequest(searchString, isRegex, matchCase, wholeWord); - assert.deepEqual(actual, expected); - } - - test('parseSearchRequest invalid', () => { - assertParseSearchResult('', true, true, true, null); - assertParseSearchResult(null, true, true, true, null); - assertParseSearchResult('(', true, false, false, null); - }); - - test('parseSearchRequest non regex', () => { - assertParseSearchResult('foo', false, false, false, /foo/gi); - assertParseSearchResult('foo', false, false, true, /\bfoo\b/gi); - assertParseSearchResult('foo', false, true, false, /foo/g); - assertParseSearchResult('foo', false, true, true, /\bfoo\b/g); - assertParseSearchResult('foo\\n', false, false, false, /foo\\n/gi); - assertParseSearchResult('foo\\\\n', false, false, false, /foo\\\\n/gi); - assertParseSearchResult('foo\\r', false, false, false, /foo\\r/gi); - assertParseSearchResult('foo\\\\r', false, false, false, /foo\\\\r/gi); - }); - - test('parseSearchRequest regex', () => { - assertParseSearchResult('foo', true, false, false, /foo/gi); - assertParseSearchResult('foo', true, false, true, /\bfoo\b/gi); - assertParseSearchResult('foo', true, true, false, /foo/g); - assertParseSearchResult('foo', true, true, true, /\bfoo\b/g); - assertParseSearchResult('foo\\n', true, false, false, /foo\n/gim); - assertParseSearchResult('foo\\\\n', true, false, false, /foo\\n/gi); - assertParseSearchResult('foo\\r', true, false, false, /foo\r/gim); - assertParseSearchResult('foo\\\\r', true, false, false, /foo\\r/gi); - }); -}); diff --git a/src/vs/editor/test/common/model/textModelSearch.test.ts b/src/vs/editor/test/common/model/textModelSearch.test.ts new file mode 100644 index 00000000000..0b87c282253 --- /dev/null +++ b/src/vs/editor/test/common/model/textModelSearch.test.ts @@ -0,0 +1,534 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { Position } from 'vs/editor/common/core/position'; +import { FindMatch } from 'vs/editor/common/editorCommon'; +import { Range } from 'vs/editor/common/core/range'; +import { TextModel } from 'vs/editor/common/model/textModel'; +import { TextModelSearch, SearchParams } from 'vs/editor/common/model/textModelSearch'; + +// --------- Find +suite('TextModelSearch', () => { + + function assertFindMatch(actual: FindMatch, expectedRange: Range, expectedMatches: string[] = null): void { + assert.deepEqual(actual, new FindMatch(expectedRange, expectedMatches)); + } + + function assertFindMatches(text: string, searchString: string, isRegex: boolean, matchCase: boolean, wholeWord: boolean, _expected: [number, number, number, number][]): void { + let expectedRanges = _expected.map(entry => new Range(entry[0], entry[1], entry[2], entry[3])); + let expectedMatches = expectedRanges.map(entry => new FindMatch(entry, null)); + let model = new TextModel([], TextModel.toRawText(text, TextModel.DEFAULT_CREATION_OPTIONS)); + + let searchParams = new SearchParams(searchString, isRegex, matchCase, wholeWord); + + let actual = TextModelSearch.findMatches(model, searchParams, model.getFullModelRange(), false, 1000); + assert.deepEqual(actual, expectedMatches, 'findMatches OK'); + + // test `findNextMatch` + let startPos = new Position(1, 1); + let match = TextModelSearch.findNextMatch(model, searchParams, startPos, false); + assert.deepEqual(match, expectedMatches[0], `findNextMatch ${startPos}`); + for (let i = 0; i < expectedMatches.length; i++) { + startPos = expectedMatches[i].range.getStartPosition();; + match = TextModelSearch.findNextMatch(model, searchParams, startPos, false); + assert.deepEqual(match, expectedMatches[i], `findNextMatch ${startPos}`); + } + + // test `findPrevMatch` + startPos = new Position(model.getLineCount(), model.getLineMaxColumn(model.getLineCount())); + match = TextModelSearch.findPreviousMatch(model, searchParams, startPos, false); + assert.deepEqual(match, expectedMatches[expectedMatches.length - 1], `findPrevMatch ${startPos}`); + for (let i = 0; i < expectedMatches.length; i++) { + startPos = expectedMatches[i].range.getEndPosition(); + match = TextModelSearch.findPreviousMatch(model, searchParams, startPos, false); + assert.deepEqual(match, expectedMatches[i], `findPrevMatch ${startPos}`); + } + + model.dispose(); + } + + let regularText = [ + 'This is some foo - bar text which contains foo and bar - as in Barcelona.', + 'Now it begins a word fooBar and now it is caps Foo-isn\'t this great?', + 'And here\'s a dull line with nothing interesting in it', + 'It is also interesting if it\'s part of a word like amazingFooBar', + 'Again nothing interesting here' + ]; + + test('Simple find', () => { + assertFindMatches( + regularText.join('\n'), + 'foo', false, false, false, + [ + [1, 14, 1, 17], + [1, 44, 1, 47], + [2, 22, 2, 25], + [2, 48, 2, 51], + [4, 59, 4, 62] + ] + ); + }); + + test('Case sensitive find', () => { + assertFindMatches( + regularText.join('\n'), + 'foo', false, true, false, + [ + [1, 14, 1, 17], + [1, 44, 1, 47], + [2, 22, 2, 25] + ] + ); + }); + + test('Whole words find', () => { + assertFindMatches( + regularText.join('\n'), + 'foo', false, false, true, + [ + [1, 14, 1, 17], + [1, 44, 1, 47], + [2, 48, 2, 51] + ] + ); + }); + + test('/^/ find', () => { + assertFindMatches( + regularText.join('\n'), + '^', true, false, false, + [ + [1, 1, 1, 1], + [2, 1, 2, 1], + [3, 1, 3, 1], + [4, 1, 4, 1], + [5, 1, 5, 1] + ] + ); + }); + + test('/$/ find', () => { + assertFindMatches( + regularText.join('\n'), + '$', true, false, false, + [ + [1, 74, 1, 74], + [2, 69, 2, 69], + [3, 54, 3, 54], + [4, 65, 4, 65], + [5, 31, 5, 31] + ] + ); + }); + + test('/.*/ find', () => { + assertFindMatches( + regularText.join('\n'), + '.*', true, false, false, + [ + [1, 1, 1, 74], + [2, 1, 2, 69], + [3, 1, 3, 54], + [4, 1, 4, 65], + [5, 1, 5, 31] + ] + ); + }); + + test('/^$/ find', () => { + assertFindMatches( + [ + 'This is some foo - bar text which contains foo and bar - as in Barcelona.', + '', + 'And here\'s a dull line with nothing interesting in it', + '', + 'Again nothing interesting here' + ].join('\n'), + '^$', true, false, false, + [ + [2, 1, 2, 1], + [4, 1, 4, 1] + ] + ); + }); + + test('multiline find 1', () => { + assertFindMatches( + [ + 'Just some text text', + 'Just some text text', + 'some text again', + 'again some text' + ].join('\n'), + 'text\\n', true, false, false, + [ + [1, 16, 2, 1], + [2, 16, 3, 1], + ] + ); + }); + + test('multiline find 2', () => { + assertFindMatches( + [ + 'Just some text text', + 'Just some text text', + 'some text again', + 'again some text' + ].join('\n'), + 'text\\nJust', true, false, false, + [ + [1, 16, 2, 5] + ] + ); + }); + + test('multiline find 3', () => { + assertFindMatches( + [ + 'Just some text text', + 'Just some text text', + 'some text again', + 'again some text' + ].join('\n'), + '\\nagain', true, false, false, + [ + [3, 16, 4, 6] + ] + ); + }); + + test('multiline find 4', () => { + assertFindMatches( + [ + 'Just some text text', + 'Just some text text', + 'some text again', + 'again some text' + ].join('\n'), + '.*\\nJust.*\\n', true, false, false, + [ + [1, 1, 3, 1] + ] + ); + }); + + test('multiline find with line beginning regex', () => { + assertFindMatches( + [ + 'if', + 'else', + '', + 'if', + 'else' + ].join('\n'), + '^if\\nelse', true, false, false, + [ + [1, 1, 2, 5], + [4, 1, 5, 5] + ] + ); + }); + + test('matching empty lines using boundary expression', () => { + assertFindMatches( + [ + 'if', + '', + 'else', + ' ', + 'if', + ' ', + 'else' + ].join('\n'), + '^\\s*$\\n', true, false, false, + [ + [2, 1, 3, 1], + [4, 1, 5, 1], + [6, 1, 7, 1] + ] + ); + }); + + test('matching lines starting with A and ending with B', () => { + assertFindMatches( + [ + 'a if b', + 'a', + 'ab', + 'eb' + ].join('\n'), + '^a.*b$', true, false, false, + [ + [1, 1, 1, 7], + [3, 1, 3, 3] + ] + ); + }); + + test('multiline find with line ending regex', () => { + assertFindMatches( + [ + 'if', + 'else', + '', + 'if', + 'elseif', + 'else' + ].join('\n'), + 'if\\nelse$', true, false, false, + [ + [1, 1, 2, 5], + [5, 5, 6, 5] + ] + ); + }); + + test('issue #4836 - ^.*$', () => { + assertFindMatches( + [ + 'Just some text text', + '', + 'some text again', + '', + 'again some text' + ].join('\n'), + '^.*$', true, false, false, + [ + [1, 1, 1, 20], + [2, 1, 2, 1], + [3, 1, 3, 16], + [4, 1, 4, 1], + [5, 1, 5, 16], + ] + ); + }); + + test('multiline find for non-regex string', () => { + assertFindMatches( + [ + 'Just some text text', + 'some text text', + 'some text again', + 'again some text', + 'but not some' + ].join('\n'), + 'text\nsome', false, false, false, + [ + [1, 16, 2, 5], + [2, 11, 3, 5], + ] + ); + }); + + test('findNextMatch without regex', () => { + let model = new TextModel([], TextModel.toRawText('line line one\nline two\nthree', TextModel.DEFAULT_CREATION_OPTIONS)); + + let searchParams = new SearchParams('line', false, false, false); + + let actual = TextModelSearch.findNextMatch(model, searchParams, { lineNumber: 1, column: 1 }, false); + assertFindMatch(actual, new Range(1, 1, 1, 5)); + + actual = TextModelSearch.findNextMatch(model, searchParams, actual.range.getEndPosition(), false); + assertFindMatch(actual, new Range(1, 6, 1, 10)); + + actual = TextModelSearch.findNextMatch(model, searchParams, { lineNumber: 1, column: 3 }, false); + assertFindMatch(actual, new Range(1, 6, 1, 10)); + + actual = TextModelSearch.findNextMatch(model, searchParams, actual.range.getEndPosition(), false); + assertFindMatch(actual, new Range(2, 1, 2, 5)); + + actual = TextModelSearch.findNextMatch(model, searchParams, actual.range.getEndPosition(), false); + assertFindMatch(actual, new Range(1, 1, 1, 5)); + + model.dispose(); + }); + + test('findNextMatch with beginning boundary regex', () => { + let model = new TextModel([], TextModel.toRawText('line one\nline two\nthree', TextModel.DEFAULT_CREATION_OPTIONS)); + + let searchParams = new SearchParams('^line', true, false, false); + + let actual = TextModelSearch.findNextMatch(model, searchParams, { lineNumber: 1, column: 1 }, false); + assertFindMatch(actual, new Range(1, 1, 1, 5)); + + actual = TextModelSearch.findNextMatch(model, searchParams, actual.range.getEndPosition(), false); + assertFindMatch(actual, new Range(2, 1, 2, 5)); + + actual = TextModelSearch.findNextMatch(model, searchParams, { lineNumber: 1, column: 3 }, false); + assertFindMatch(actual, new Range(2, 1, 2, 5)); + + actual = TextModelSearch.findNextMatch(model, searchParams, actual.range.getEndPosition(), false); + assertFindMatch(actual, new Range(1, 1, 1, 5)); + + model.dispose(); + }); + + test('findNextMatch with beginning boundary regex and line has repetitive beginnings', () => { + let model = new TextModel([], TextModel.toRawText('line line one\nline two\nthree', TextModel.DEFAULT_CREATION_OPTIONS)); + + let searchParams = new SearchParams('^line', true, false, false); + + let actual = TextModelSearch.findNextMatch(model, searchParams, { lineNumber: 1, column: 1 }, false); + assertFindMatch(actual, new Range(1, 1, 1, 5)); + + actual = TextModelSearch.findNextMatch(model, searchParams, actual.range.getEndPosition(), false); + assertFindMatch(actual, new Range(2, 1, 2, 5)); + + actual = TextModelSearch.findNextMatch(model, searchParams, { lineNumber: 1, column: 3 }, false); + assertFindMatch(actual, new Range(2, 1, 2, 5)); + + actual = TextModelSearch.findNextMatch(model, searchParams, actual.range.getEndPosition(), false); + assertFindMatch(actual, new Range(1, 1, 1, 5)); + + model.dispose(); + }); + + test('findNextMatch with beginning boundary multiline regex and line has repetitive beginnings', () => { + let model = new TextModel([], TextModel.toRawText('line line one\nline two\nline three\nline four', TextModel.DEFAULT_CREATION_OPTIONS)); + + let searchParams = new SearchParams('^line.*\\nline', true, false, false); + + let actual = TextModelSearch.findNextMatch(model, searchParams, { lineNumber: 1, column: 1 }, false); + assertFindMatch(actual, new Range(1, 1, 2, 5)); + + actual = TextModelSearch.findNextMatch(model, searchParams, actual.range.getEndPosition(), false); + assertFindMatch(actual, new Range(3, 1, 4, 5)); + + actual = TextModelSearch.findNextMatch(model, searchParams, { lineNumber: 2, column: 1 }, false); + assertFindMatch(actual, new Range(2, 1, 3, 5)); + + model.dispose(); + }); + + test('findNextMatch with ending boundary regex', () => { + let model = new TextModel([], TextModel.toRawText('one line line\ntwo line\nthree', TextModel.DEFAULT_CREATION_OPTIONS)); + + let searchParams = new SearchParams('line$', true, false, false); + + let actual = TextModelSearch.findNextMatch(model, searchParams, { lineNumber: 1, column: 1 }, false); + assertFindMatch(actual, new Range(1, 10, 1, 14)); + + actual = TextModelSearch.findNextMatch(model, searchParams, { lineNumber: 1, column: 4 }, false); + assertFindMatch(actual, new Range(1, 10, 1, 14)); + + actual = TextModelSearch.findNextMatch(model, searchParams, actual.range.getEndPosition(), false); + assertFindMatch(actual, new Range(2, 5, 2, 9)); + + actual = TextModelSearch.findNextMatch(model, searchParams, actual.range.getEndPosition(), false); + assertFindMatch(actual, new Range(1, 10, 1, 14)); + + model.dispose(); + }); + + test('findMatches with capturing matches', () => { + let model = new TextModel([], TextModel.toRawText('one line line\ntwo line\nthree', TextModel.DEFAULT_CREATION_OPTIONS)); + + let searchParams = new SearchParams('(l(in)e)', true, false, false); + + let actual = TextModelSearch.findMatches(model, searchParams, model.getFullModelRange(), true, 100); + assert.deepEqual(actual, [ + new FindMatch(new Range(1, 5, 1, 9), ['line', 'line', 'in']), + new FindMatch(new Range(1, 10, 1, 14), ['line', 'line', 'in']), + new FindMatch(new Range(2, 5, 2, 9), ['line', 'line', 'in']), + ]); + + model.dispose(); + }); + + test('findMatches multiline with capturing matches', () => { + let model = new TextModel([], TextModel.toRawText('one line line\ntwo line\nthree', TextModel.DEFAULT_CREATION_OPTIONS)); + + let searchParams = new SearchParams('(l(in)e)\\n', true, false, false); + + let actual = TextModelSearch.findMatches(model, searchParams, model.getFullModelRange(), true, 100); + assert.deepEqual(actual, [ + new FindMatch(new Range(1, 10, 2, 1), ['line\n', 'line', 'in']), + new FindMatch(new Range(2, 5, 3, 1), ['line\n', 'line', 'in']), + ]); + + model.dispose(); + }); + + test('findNextMatch with capturing matches', () => { + let model = new TextModel([], TextModel.toRawText('one line line\ntwo line\nthree', TextModel.DEFAULT_CREATION_OPTIONS)); + + let searchParams = new SearchParams('(l(in)e)', true, false, false); + + let actual = TextModelSearch.findNextMatch(model, searchParams, new Position(1, 1), true); + assertFindMatch(actual, new Range(1, 5, 1, 9), ['line', 'line', 'in']); + + model.dispose(); + }); + + test('findNextMatch multiline with capturing matches', () => { + let model = new TextModel([], TextModel.toRawText('one line line\ntwo line\nthree', TextModel.DEFAULT_CREATION_OPTIONS)); + + let searchParams = new SearchParams('(l(in)e)\\n', true, false, false); + + let actual = TextModelSearch.findNextMatch(model, searchParams, new Position(1, 1), true); + assertFindMatch(actual, new Range(1, 10, 2, 1), ['line\n', 'line', 'in']); + + model.dispose(); + }); + + test('findPreviousMatch with capturing matches', () => { + let model = new TextModel([], TextModel.toRawText('one line line\ntwo line\nthree', TextModel.DEFAULT_CREATION_OPTIONS)); + + let searchParams = new SearchParams('(l(in)e)', true, false, false); + + let actual = TextModelSearch.findPreviousMatch(model, searchParams, new Position(1, 1), true); + assertFindMatch(actual, new Range(2, 5, 2, 9), ['line', 'line', 'in']); + + model.dispose(); + }); + + test('findPreviousMatch multiline with capturing matches', () => { + let model = new TextModel([], TextModel.toRawText('one line line\ntwo line\nthree', TextModel.DEFAULT_CREATION_OPTIONS)); + + let searchParams = new SearchParams('(l(in)e)\\n', true, false, false); + + let actual = TextModelSearch.findPreviousMatch(model, searchParams, new Position(1, 1), true); + assertFindMatch(actual, new Range(2, 5, 3, 1), ['line\n', 'line', 'in']); + + model.dispose(); + }); + + function assertParseSearchResult(searchString: string, isRegex: boolean, matchCase: boolean, wholeWord: boolean, expected: RegExp): void { + let searchParams = new SearchParams(searchString, isRegex, matchCase, wholeWord); + let actual = searchParams.parseSearchRequest(); + assert.deepEqual(actual, expected); + } + + test('parseSearchRequest invalid', () => { + assertParseSearchResult('', true, true, true, null); + assertParseSearchResult(null, true, true, true, null); + assertParseSearchResult('(', true, false, false, null); + }); + + test('parseSearchRequest non regex', () => { + assertParseSearchResult('foo', false, false, false, /foo/gi); + assertParseSearchResult('foo', false, false, true, /\bfoo\b/gi); + assertParseSearchResult('foo', false, true, false, /foo/g); + assertParseSearchResult('foo', false, true, true, /\bfoo\b/g); + assertParseSearchResult('foo\\n', false, false, false, /foo\\n/gi); + assertParseSearchResult('foo\\\\n', false, false, false, /foo\\\\n/gi); + assertParseSearchResult('foo\\r', false, false, false, /foo\\r/gi); + assertParseSearchResult('foo\\\\r', false, false, false, /foo\\\\r/gi); + }); + + test('parseSearchRequest regex', () => { + assertParseSearchResult('foo', true, false, false, /foo/gi); + assertParseSearchResult('foo', true, false, true, /\bfoo\b/gi); + assertParseSearchResult('foo', true, true, false, /foo/g); + assertParseSearchResult('foo', true, true, true, /\bfoo\b/g); + assertParseSearchResult('foo\\n', true, false, false, /foo\n/gim); + assertParseSearchResult('foo\\\\n', true, false, false, /foo\\n/gi); + assertParseSearchResult('foo\\r', true, false, false, /foo\r/gim); + assertParseSearchResult('foo\\\\r', true, false, false, /foo\\r/gi); + }); +}); diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 9c443a21a98..f4f46ba3155 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -2089,6 +2089,12 @@ declare module monaco.editor { findPreviousMatch(searchString: string, searchStart: IPosition, isRegex: boolean, matchCase: boolean, wholeWord: boolean): Range; } + export class FindMatch { + _findMatchBrand: void; + readonly range: Range; + readonly matches: string[]; + } + export interface IReadOnlyModel extends ITextModel { /** * Gets the resource associated with this editor model. diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index 1e5d0033a8c..470459a4f4d 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -171,7 +171,7 @@ export class ExtensionManagementService implements IExtensionManagementService { nls.localize('install', "Yes"), nls.localize('doNotInstall', "No") ]; - return this.choiceService.choose(Severity.Info, message, options) + return this.choiceService.choose(Severity.Info, message, options, true) .then(value => { if (value === 0) { return this.installWithDependencies(compatibleVersion); diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index 5a7cc0c5389..cbd1ad7f30c 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -2396,6 +2396,13 @@ declare module 'vscode' { */ range?: Range; + /** + * An optional set of characters that when pressed while this completion is active will accept it first and + * then type that character. *Note* that all commit characters should have `length=1` and that superfluous + * characters will be ignored. + */ + commitCharacters?: string[]; + /** * @deprecated **Deprecated** in favor of `CompletionItem.insertText` and `CompletionItem.range`. * @@ -2814,7 +2821,7 @@ declare module 'vscode' { /** * Readable dictionary that backs this configuration. */ - readonly [key: string]: any; + readonly[key: string]: any; } /** @@ -3669,6 +3676,40 @@ declare module 'vscode' { * @return A new Terminal. */ export function createTerminal(name?: string, shellPath?: string, shellArgs?: string[]): Terminal; + + /** + * Creates a [Terminal](#Terminal). The cwd of the terminal will be the workspace directory + * if it exists, regardless of whether an explicit customStartPath setting exists. + * + * @param options A TerminalOptions object describing the characteristics of the new terminal. + * @return A new Terminal. + */ + export function createTerminal(options: TerminalOptions): Terminal; + } + + /** + * Value-object describing what options formatting should use. + */ + export interface TerminalOptions { + /** + * A human-readable string which will be used to represent the terminal in the UI. + */ + name?: string; + + /** + * A path to a custom shell executable to be used in the terminal. + */ + shellPath?: string; + + /** + * Args for the custom shell executable, this does not work on Windows (see #8429) + */ + shellArgs?: string[]; + + /** + * Whether the terminal should wait on exit for a keypress before closing the instance. + */ + waitOnExit?: boolean; } /** diff --git a/src/vs/workbench/api/node/extHost.api.impl.ts b/src/vs/workbench/api/node/extHost.api.impl.ts index ba77d1b701c..7a81523517c 100644 --- a/src/vs/workbench/api/node/extHost.api.impl.ts +++ b/src/vs/workbench/api/node/extHost.api.impl.ts @@ -125,7 +125,7 @@ export function createApiFactory(initData: IInitData, threadService: IThreadServ if (extension.enableProposedApi) { - if (!initData.environment.enableProposedApi) { + if (!initData.environment.enableProposedApi && !extension.isBuiltin) { extension.enableProposedApi = false; console.warn('PROPOSED API is only available when developing an extension'); @@ -306,8 +306,11 @@ export function createApiFactory(initData: IInitData, threadService: IThreadServ createOutputChannel(name: string): vscode.OutputChannel { return extHostOutputService.createOutputChannel(name); }, - createTerminal(name?: string, shellPath?: string, shellArgs?: string[]): vscode.Terminal { - return extHostTerminalService.createTerminal(name, shellPath, shellArgs); + createTerminal(nameOrOptions: vscode.TerminalOptions | string, shellPath?: string, shellArgs?: string[]): vscode.Terminal { + if (typeof nameOrOptions === 'object') { + return extHostTerminalService.createTerminalFromOptions(nameOrOptions); + } + return extHostTerminalService.createTerminal(nameOrOptions, shellPath, shellArgs); }, // proposed API sampleFunction: proposedApiFunction(extension, () => { diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index a6d072ebd4f..571f357ec09 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -184,7 +184,7 @@ export abstract class MainThreadOutputServiceShape { } export abstract class MainThreadTerminalServiceShape { - $createTerminal(name?: string, shellPath?: string, shellArgs?: string[]): TPromise { throw ni(); } + $createTerminal(name?: string, shellPath?: string, shellArgs?: string[], waitOnExit?: boolean): TPromise { throw ni(); } $dispose(terminalId: number): void { throw ni(); } $hide(terminalId: number): void { throw ni(); } $sendText(terminalId: number, text: string, addNewLine: boolean): void { throw ni(); } diff --git a/src/vs/workbench/api/node/extHostLanguageFeatures.ts b/src/vs/workbench/api/node/extHostLanguageFeatures.ts index 38706f8abb2..6128e8165c2 100644 --- a/src/vs/workbench/api/node/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/node/extHostLanguageFeatures.ts @@ -514,7 +514,8 @@ class SuggestAdapter { // insertText: undefined, additionalTextEdits: item.additionalTextEdits && item.additionalTextEdits.map(TypeConverters.TextEdit.from), - command: this._commands.toInternal(item.command) + command: this._commands.toInternal(item.command), + commitCharacters: item.commitCharacters }; // 'insertText'-logic diff --git a/src/vs/workbench/api/node/extHostTerminalService.ts b/src/vs/workbench/api/node/extHostTerminalService.ts index 49229564694..cfd48c75932 100644 --- a/src/vs/workbench/api/node/extHostTerminalService.ts +++ b/src/vs/workbench/api/node/extHostTerminalService.ts @@ -20,14 +20,20 @@ export class ExtHostTerminal implements vscode.Terminal { private _pidPromise: TPromise; private _pidPromiseComplete: TValueCallback; - constructor(proxy: MainThreadTerminalServiceShape, name?: string, shellPath?: string, shellArgs?: string[]) { + constructor( + proxy: MainThreadTerminalServiceShape, + name?: string, + shellPath?: string, + shellArgs?: string[], + waitOnExit?: boolean + ) { this._name = name; this._queuedRequests = []; this._proxy = proxy; this._pidPromise = new TPromise(c => { this._pidPromiseComplete = c; }); - this._proxy.$createTerminal(name, shellPath, shellArgs).then((id) => { + this._proxy.$createTerminal(name, shellPath, shellArgs, waitOnExit).then((id) => { this._id = id; this._queuedRequests.forEach((r) => { r.run(this._proxy, this._id); @@ -107,6 +113,12 @@ export class ExtHostTerminalService implements ExtHostTerminalServiceShape { return terminal; } + public createTerminalFromOptions(options: vscode.TerminalOptions): vscode.Terminal { + let terminal = new ExtHostTerminal(this._proxy, options.name, options.shellPath, options.shellArgs, options.waitOnExit); + this._terminals.push(terminal); + return terminal; + } + public get onDidCloseTerminal(): Event { return this._onDidCloseTerminal && this._onDidCloseTerminal.event; } diff --git a/src/vs/workbench/api/node/mainThreadOutputService.ts b/src/vs/workbench/api/node/mainThreadOutputService.ts index 313e7c28dd1..bbaacb49b47 100644 --- a/src/vs/workbench/api/node/mainThreadOutputService.ts +++ b/src/vs/workbench/api/node/mainThreadOutputService.ts @@ -53,7 +53,7 @@ export class MainThreadOutputService extends MainThreadOutputServiceShape { public $close(channelId: string): TPromise { const panel = this._panelService.getActivePanel(); if (panel && panel.getId() === OUTPUT_PANEL_ID && channelId === this._outputService.getActiveChannel().id) { - this._partService.setPanelHidden(true); + return this._partService.setPanelHidden(true); } return undefined; diff --git a/src/vs/workbench/api/node/mainThreadTerminalService.ts b/src/vs/workbench/api/node/mainThreadTerminalService.ts index 6f1f99702a0..75baf259baa 100644 --- a/src/vs/workbench/api/node/mainThreadTerminalService.ts +++ b/src/vs/workbench/api/node/mainThreadTerminalService.ts @@ -30,8 +30,8 @@ export class MainThreadTerminalService extends MainThreadTerminalServiceShape { this._toDispose = dispose(this._toDispose); } - public $createTerminal(name?: string, shellPath?: string, shellArgs?: string[]): TPromise { - return TPromise.as(this.terminalService.createInstance(name, shellPath, shellArgs, true).id); + public $createTerminal(name?: string, shellPath?: string, shellArgs?: string[], waitOnExit?: boolean): TPromise { + return TPromise.as(this.terminalService.createInstance(name, shellPath, shellArgs, waitOnExit, true).id); } public $show(terminalId: number, preserveFocus: boolean): void { diff --git a/src/vs/workbench/browser/actions/toggleSidebarVisibility.ts b/src/vs/workbench/browser/actions/toggleSidebarVisibility.ts index e31853b1973..a5d1f667ac7 100644 --- a/src/vs/workbench/browser/actions/toggleSidebarVisibility.ts +++ b/src/vs/workbench/browser/actions/toggleSidebarVisibility.ts @@ -30,9 +30,7 @@ export class ToggleSidebarVisibilityAction extends Action { public run(): TPromise { const hideSidebar = this.partService.isVisible(Parts.SIDEBAR_PART); - this.partService.setSideBarHidden(hideSidebar); - - return TPromise.as(null); + return this.partService.setSideBarHidden(hideSidebar); } } diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index 09ac060b7f4..cd74b564aaf 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -5,6 +5,8 @@ '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 { Sash, ISashEvent, IVerticalSashLayoutProvider, IHorizontalSashLayoutProvider, Orientation } from 'vs/base/browser/ui/sash/sash'; @@ -18,15 +20,18 @@ import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService'; import { getZoomFactor } from 'vs/base/browser/browser'; -const DEFAULT_MIN_SIDEBAR_PART_WIDTH = 170; -const DEFAULT_MIN_PANEL_PART_HEIGHT = 77; -const DEFAULT_MIN_EDITOR_PART_HEIGHT = 70; -const DEFAULT_MIN_EDITOR_PART_WIDTH = 220; +const MIN_SIDEBAR_PART_WIDTH = 170; +const MIN_EDITOR_PART_HEIGHT = 70; +const MIN_EDITOR_PART_WIDTH = 220; +const MIN_PANEL_PART_HEIGHT = 77; const DEFAULT_PANEL_HEIGHT_COEFFICIENT = 0.4; const HIDE_SIDEBAR_WIDTH_THRESHOLD = 50; const HIDE_PANEL_HEIGHT_THRESHOLD = 50; +const TITLE_BAR_HEIGHT = 22; +const STATUS_BAR_HEIGHT = 22; +const ACTIVITY_BAR_WIDTH = 50; -interface ComputedStyles { +interface PartLayoutInfo { titlebar: { height: number; }; activitybar: { width: number; }; sidebar: { minWidth: number; }; @@ -53,8 +58,7 @@ export class WorkbenchLayout implements IVerticalSashLayoutProvider, IHorizontal private statusbar: Part; private quickopen: QuickOpenController; private toUnbind: IDisposable[]; - private computedStyles: ComputedStyles; - private initialComputedStyles: ComputedStyles; + private partLayoutInfo: PartLayoutInfo; private workbenchSize: Dimension; private sashX: Sash; private sashY: Sash; @@ -101,7 +105,7 @@ export class WorkbenchLayout implements IVerticalSashLayoutProvider, IHorizontal this.statusbar = parts.statusbar; this.quickopen = quickopen; this.toUnbind = []; - this.computedStyles = null; + this.partLayoutInfo = this.getPartLayoutInfo(); this.panelHeightBeforeMaximized = 0; this.sashX = new Sash(this.workbenchContainer.getHTMLElement(), this, { @@ -118,13 +122,37 @@ export class WorkbenchLayout implements IVerticalSashLayoutProvider, IHorizontal this.layoutEditorGroupsVertically = (this.editorGroupService.getGroupOrientation() !== 'horizontal'); - this.toUnbind.push(themeService.onDidColorThemeChange(_ => this.relayout())); + this.toUnbind.push(themeService.onDidColorThemeChange(_ => this.layout())); this.toUnbind.push(editorGroupService.onEditorsChanged(() => this.onEditorsChanged())); this.toUnbind.push(editorGroupService.onGroupOrientationChanged(e => this.onGroupOrientationChanged())); this.registerSashListeners(); } + private getPartLayoutInfo(): PartLayoutInfo { + return { + titlebar: { + height: TITLE_BAR_HEIGHT + }, + activitybar: { + width: ACTIVITY_BAR_WIDTH + }, + sidebar: { + minWidth: MIN_SIDEBAR_PART_WIDTH + }, + panel: { + minHeight: MIN_PANEL_PART_HEIGHT + }, + editor: { + minWidth: MIN_EDITOR_PART_WIDTH, + minHeight: MIN_EDITOR_PART_HEIGHT + }, + statusbar: { + height: STATUS_BAR_HEIGHT + } + }; + } + private registerSashListeners(): void { let startX: number = 0; let startY: number = 0; @@ -144,37 +172,38 @@ export class WorkbenchLayout implements IVerticalSashLayoutProvider, IHorizontal let sidebarPosition = this.partService.getSideBarPosition(); let isSidebarVisible = this.partService.isVisible(Parts.SIDEBAR_PART); let newSashWidth = (sidebarPosition === Position.LEFT) ? this.startSidebarWidth + e.currentX - startX : this.startSidebarWidth - e.currentX + startX; + let promise = TPromise.as(null); // Sidebar visible if (isSidebarVisible) { // Automatically hide side bar when a certain threshold is met - if (newSashWidth + HIDE_SIDEBAR_WIDTH_THRESHOLD < this.computedStyles.sidebar.minWidth) { - let dragCompensation = DEFAULT_MIN_SIDEBAR_PART_WIDTH - HIDE_SIDEBAR_WIDTH_THRESHOLD; - this.partService.setSideBarHidden(true); + if (newSashWidth + HIDE_SIDEBAR_WIDTH_THRESHOLD < this.partLayoutInfo.sidebar.minWidth) { + let dragCompensation = MIN_SIDEBAR_PART_WIDTH - HIDE_SIDEBAR_WIDTH_THRESHOLD; + promise = this.partService.setSideBarHidden(true); startX = (sidebarPosition === Position.LEFT) ? Math.max(this.activitybarWidth, e.currentX - dragCompensation) : Math.min(e.currentX + dragCompensation, this.workbenchSize.width - this.activitybarWidth); this.sidebarWidth = this.startSidebarWidth; // when restoring sidebar, restore to the sidebar width we started from } // Otherwise size the sidebar accordingly else { - this.sidebarWidth = Math.max(this.computedStyles.sidebar.minWidth, newSashWidth); // Sidebar can not become smaller than MIN_PART_WIDTH - doLayout = newSashWidth >= this.computedStyles.sidebar.minWidth; + this.sidebarWidth = Math.max(this.partLayoutInfo.sidebar.minWidth, newSashWidth); // Sidebar can not become smaller than MIN_PART_WIDTH + doLayout = newSashWidth >= this.partLayoutInfo.sidebar.minWidth; } } // Sidebar hidden else { - if ((sidebarPosition === Position.LEFT && e.currentX - startX >= this.computedStyles.sidebar.minWidth) || - (sidebarPosition === Position.RIGHT && startX - e.currentX >= this.computedStyles.sidebar.minWidth)) { - this.startSidebarWidth = this.computedStyles.sidebar.minWidth - (sidebarPosition === Position.LEFT ? e.currentX - startX : startX - e.currentX); - this.sidebarWidth = this.computedStyles.sidebar.minWidth; - this.partService.setSideBarHidden(false); + if ((sidebarPosition === Position.LEFT && e.currentX - startX >= this.partLayoutInfo.sidebar.minWidth) || + (sidebarPosition === Position.RIGHT && startX - e.currentX >= this.partLayoutInfo.sidebar.minWidth)) { + this.startSidebarWidth = this.partLayoutInfo.sidebar.minWidth - (sidebarPosition === Position.LEFT ? e.currentX - startX : startX - e.currentX); + this.sidebarWidth = this.partLayoutInfo.sidebar.minWidth; + promise = this.partService.setSideBarHidden(false); } } if (doLayout) { - this.layout(); + promise.done(() => this.layout(), errors.onUnexpectedError); } }); @@ -182,36 +211,37 @@ export class WorkbenchLayout implements IVerticalSashLayoutProvider, IHorizontal let doLayout = false; let isPanelVisible = this.partService.isVisible(Parts.PANEL_PART); let newSashHeight = this.startPanelHeight - (e.currentY - startY); + let promise = TPromise.as(null); // Panel visible if (isPanelVisible) { // Automatically hide panel when a certain threshold is met - if (newSashHeight + HIDE_PANEL_HEIGHT_THRESHOLD < this.computedStyles.panel.minHeight) { - let dragCompensation = DEFAULT_MIN_PANEL_PART_HEIGHT - HIDE_PANEL_HEIGHT_THRESHOLD; - this.partService.setPanelHidden(true); + if (newSashHeight + HIDE_PANEL_HEIGHT_THRESHOLD < this.partLayoutInfo.panel.minHeight) { + let dragCompensation = MIN_PANEL_PART_HEIGHT - HIDE_PANEL_HEIGHT_THRESHOLD; + promise = this.partService.setPanelHidden(true); startY = Math.min(this.sidebarHeight - this.statusbarHeight - this.titlebarHeight, e.currentY + dragCompensation); this.panelHeight = this.startPanelHeight; // when restoring panel, restore to the panel height we started from } // Otherwise size the panel accordingly else { - this.panelHeight = Math.max(this.computedStyles.panel.minHeight, newSashHeight); // Panel can not become smaller than MIN_PART_HEIGHT - doLayout = newSashHeight >= this.computedStyles.panel.minHeight; + this.panelHeight = Math.max(this.partLayoutInfo.panel.minHeight, newSashHeight); // Panel can not become smaller than MIN_PART_HEIGHT + doLayout = newSashHeight >= this.partLayoutInfo.panel.minHeight; } } // Panel hidden else { - if (startY - e.currentY >= this.computedStyles.panel.minHeight) { + if (startY - e.currentY >= this.partLayoutInfo.panel.minHeight) { this.startPanelHeight = 0; - this.panelHeight = this.computedStyles.panel.minHeight; - this.partService.setPanelHidden(false); + this.panelHeight = this.partLayoutInfo.panel.minHeight; + promise = this.partService.setPanelHidden(false); } } if (doLayout) { - this.layout(); + promise.done(() => this.layout(), errors.onUnexpectedError); } }); @@ -226,17 +256,15 @@ export class WorkbenchLayout implements IVerticalSashLayoutProvider, IHorizontal this.sashY.addListener2('reset', () => { this.panelHeight = this.sidebarHeight * DEFAULT_PANEL_HEIGHT_COEFFICIENT; this.storageService.store(WorkbenchLayout.sashYHeightSettingsKey, this.panelHeight, StorageScope.GLOBAL); - this.partService.setPanelHidden(false); - this.layout(); + this.partService.setPanelHidden(false).done(() => this.layout(), errors.onUnexpectedError); }); this.sashX.addListener2('reset', () => { let activeViewlet = this.viewletService.getActiveViewlet(); let optimalWidth = activeViewlet && activeViewlet.getOptimalWidth(); - this.sidebarWidth = Math.max(DEFAULT_MIN_SIDEBAR_PART_WIDTH, optimalWidth || 0); + this.sidebarWidth = Math.max(MIN_SIDEBAR_PART_WIDTH, optimalWidth || 0); this.storageService.store(WorkbenchLayout.sashXWidthSettingsKey, this.sidebarWidth, StorageScope.GLOBAL); - this.partService.setSideBarHidden(false); - this.layout(); + this.partService.setSideBarHidden(false).done(() => this.layout(), errors.onUnexpectedError); }); } @@ -248,8 +276,8 @@ export class WorkbenchLayout implements IVerticalSashLayoutProvider, IHorizontal if (this.workbenchSize && (this.sidebarWidth || this.panelHeight)) { let visibleEditors = this.editorService.getVisibleEditors().length; if (visibleEditors > 1) { - const sidebarOverflow = this.layoutEditorGroupsVertically && (this.workbenchSize.width - this.sidebarWidth < visibleEditors * DEFAULT_MIN_EDITOR_PART_WIDTH); - const panelOverflow = !this.layoutEditorGroupsVertically && (this.workbenchSize.height - this.panelHeight < visibleEditors * DEFAULT_MIN_EDITOR_PART_HEIGHT); + const sidebarOverflow = this.layoutEditorGroupsVertically && (this.workbenchSize.width - this.sidebarWidth < visibleEditors * MIN_EDITOR_PART_WIDTH); + const panelOverflow = !this.layoutEditorGroupsVertically && (this.workbenchSize.height - this.panelHeight < visibleEditors * MIN_EDITOR_PART_HEIGHT); if (sidebarOverflow || panelOverflow) { this.layout(); @@ -269,68 +297,8 @@ export class WorkbenchLayout implements IVerticalSashLayoutProvider, IHorizontal } } - private relayout(): void { - - // Recompute Styles - this.computeStyle(); - this.editor.getLayout().computeStyle(); - this.sidebar.getLayout().computeStyle(); - this.panel.getLayout().computeStyle(); - - // Trigger Layout - this.layout(); - } - - private computeStyle(): void { - const titlebarStyle = this.titlebar.getContainer().getComputedStyle(); - const sidebarStyle = this.sidebar.getContainer().getComputedStyle(); - const panelStyle = this.panel.getContainer().getComputedStyle(); - const editorStyle = this.editor.getContainer().getComputedStyle(); - const activitybarStyle = this.activitybar.getContainer().getComputedStyle(); - const statusbarStyle = this.statusbar.getContainer().getComputedStyle(); - - // Determine styles by looking into their CSS - this.computedStyles = { - titlebar: { - height: parseInt(titlebarStyle.getPropertyValue('height'), 10) - }, - activitybar: { - width: parseInt(activitybarStyle.getPropertyValue('width'), 10) - }, - sidebar: { - minWidth: parseInt(sidebarStyle.getPropertyValue('min-width'), 10) || DEFAULT_MIN_SIDEBAR_PART_WIDTH - }, - panel: { - minHeight: parseInt(panelStyle.getPropertyValue('min-height'), 10) || DEFAULT_MIN_PANEL_PART_HEIGHT - }, - editor: { - minWidth: parseInt(editorStyle.getPropertyValue('min-width'), 10) || DEFAULT_MIN_EDITOR_PART_WIDTH, - minHeight: DEFAULT_MIN_EDITOR_PART_HEIGHT - }, - statusbar: { - height: parseInt(statusbarStyle.getPropertyValue('height'), 10) - } - }; - - // Always keep the initial computed styles - if (!this.initialComputedStyles) { - this.initialComputedStyles = this.computedStyles; - } - } - public layout(options?: ILayoutOptions): void { - if (options && options.forceStyleRecompute) { - this.computeStyle(); - this.editor.getLayout().computeStyle(); - this.sidebar.getLayout().computeStyle(); - this.panel.getLayout().computeStyle(); - } - - if (!this.computedStyles) { - this.computeStyle(); - } - - this.workbenchSize = this.getWorkbenchArea(); + this.workbenchSize = this.parent.getClientArea(); const isActivityBarHidden = !this.partService.isVisible(Parts.ACTIVITYBAR_PART); const isTitlebarHidden = !this.partService.isVisible(Parts.TITLEBAR_PART); @@ -344,35 +312,35 @@ export class WorkbenchLayout implements IVerticalSashLayoutProvider, IHorizontal if (isSidebarHidden) { sidebarWidth = 0; } else if (this.sidebarWidth !== -1) { - sidebarWidth = Math.max(this.computedStyles.sidebar.minWidth, this.sidebarWidth); + sidebarWidth = Math.max(this.partLayoutInfo.sidebar.minWidth, this.sidebarWidth); } else { sidebarWidth = this.workbenchSize.width / 5; this.sidebarWidth = sidebarWidth; } - this.statusbarHeight = isStatusbarHidden ? 0 : this.computedStyles.statusbar.height; - this.titlebarHeight = isTitlebarHidden ? 0 : this.initialComputedStyles.titlebar.height / getZoomFactor(); // adjust for zoom prevention + this.statusbarHeight = isStatusbarHidden ? 0 : this.partLayoutInfo.statusbar.height; + this.titlebarHeight = isTitlebarHidden ? 0 : this.partLayoutInfo.titlebar.height / getZoomFactor(); // adjust for zoom prevention this.sidebarHeight = this.workbenchSize.height - this.statusbarHeight - this.titlebarHeight; let sidebarSize = new Dimension(sidebarWidth, this.sidebarHeight); // Activity Bar - this.activitybarWidth = isActivityBarHidden ? 0 : this.computedStyles.activitybar.width; + this.activitybarWidth = isActivityBarHidden ? 0 : this.partLayoutInfo.activitybar.width; let activityBarSize = new Dimension(this.activitybarWidth, sidebarSize.height); // Panel part let panelHeight: number; - const maxPanelHeight = sidebarSize.height - DEFAULT_MIN_EDITOR_PART_HEIGHT; + const maxPanelHeight = sidebarSize.height - MIN_EDITOR_PART_HEIGHT; if (isPanelHidden) { panelHeight = 0; } else if (this.panelHeight > 0) { - panelHeight = Math.min(maxPanelHeight, Math.max(this.computedStyles.panel.minHeight, this.panelHeight)); + panelHeight = Math.min(maxPanelHeight, Math.max(this.partLayoutInfo.panel.minHeight, this.panelHeight)); } else { panelHeight = sidebarSize.height * DEFAULT_PANEL_HEIGHT_COEFFICIENT; } if (options && options.toggleMaximizedPanel) { const heightToSwap = panelHeight; - panelHeight = panelHeight === maxPanelHeight ? Math.max(this.computedStyles.panel.minHeight, Math.min(this.panelHeightBeforeMaximized, maxPanelHeight)) : maxPanelHeight; + panelHeight = panelHeight === maxPanelHeight ? Math.max(this.partLayoutInfo.panel.minHeight, Math.min(this.panelHeightBeforeMaximized, maxPanelHeight)) : maxPanelHeight; this.panelHeightBeforeMaximized = heightToSwap; } const panelDimension = new Dimension(this.workbenchSize.width - sidebarSize.width - activityBarSize.width, panelHeight); @@ -403,8 +371,8 @@ export class WorkbenchLayout implements IVerticalSashLayoutProvider, IHorizontal } // Assert Sidebar and Editor Size to not overflow - let editorMinWidth = this.computedStyles.editor.minWidth; - let editorMinHeight = this.computedStyles.editor.minHeight; + let editorMinWidth = this.partLayoutInfo.editor.minWidth; + let editorMinHeight = this.partLayoutInfo.editor.minHeight; let visibleEditorCount = this.editorService.getVisibleEditors().length; if (visibleEditorCount > 1) { if (this.layoutEditorGroupsVertically) { @@ -419,14 +387,14 @@ export class WorkbenchLayout implements IVerticalSashLayoutProvider, IHorizontal editorSize.width = editorMinWidth; panelDimension.width = editorMinWidth; sidebarSize.width -= diff; - sidebarSize.width = Math.max(DEFAULT_MIN_SIDEBAR_PART_WIDTH, sidebarSize.width); + sidebarSize.width = Math.max(MIN_SIDEBAR_PART_WIDTH, sidebarSize.width); } if (editorSize.height < editorMinHeight) { let diff = editorMinHeight - editorSize.height; editorSize.height = editorMinHeight; panelDimension.height -= diff; - panelDimension.height = Math.max(DEFAULT_MIN_PANEL_PART_HEIGHT, panelDimension.height); + panelDimension.height = Math.max(MIN_PANEL_PART_HEIGHT, panelDimension.height); } if (!isSidebarHidden) { @@ -526,15 +494,6 @@ export class WorkbenchLayout implements IVerticalSashLayoutProvider, IHorizontal this.contextViewService.layout(); } - private getWorkbenchArea(): Dimension { - - // Client Area: Parent - let clientArea = this.parent.getClientArea(); - - // Workbench: Client Area - Margins - return clientArea; - } - public getVerticalSashTop(sash: Sash): number { return this.titlebarHeight; } diff --git a/src/vs/workbench/browser/panel.ts b/src/vs/workbench/browser/panel.ts index 862686efabe..7db1606d256 100644 --- a/src/vs/workbench/browser/panel.ts +++ b/src/vs/workbench/browser/panel.ts @@ -84,8 +84,7 @@ export abstract class TogglePanelAction extends Action { public run(): TPromise { if (this.isPanelShowing()) { - this.partService.setPanelHidden(true); - return TPromise.as(true); + return this.partService.setPanelHidden(true); } return this.panelService.openPanel(this.panelId, true); diff --git a/src/vs/workbench/browser/part.ts b/src/vs/workbench/browser/part.ts index 417066997c5..89622e3fa6e 100644 --- a/src/vs/workbench/browser/part.ts +++ b/src/vs/workbench/browser/part.ts @@ -9,18 +9,21 @@ import 'vs/css!./media/part'; import { Dimension, Builder } from 'vs/base/browser/builder'; import { WorkbenchComponent } from 'vs/workbench/common/component'; +export interface IPartOptions { + hasTitle?: boolean; +} + /** - * Parts are layed out in the workbench and have their own layout that arranges a title, - * content and status area to show content. + * Parts are layed out in the workbench and have their own layout that arranges an optional title + * and mandatory content area to show content. */ export abstract class Part extends WorkbenchComponent { private parent: Builder; private titleArea: Builder; private contentArea: Builder; - private statusArea: Builder; private partLayout: PartLayout; - constructor(id: string) { + constructor(id: string, private options: IPartOptions) { super(id); } @@ -28,15 +31,14 @@ export abstract class Part extends WorkbenchComponent { * Note: Clients should not call this method, the workbench calls this * method. Calling it otherwise may result in unexpected behavior. * - * Called to create title, content and status area of the part. + * Called to create title and content area of the part. */ public create(parent: Builder): void { this.parent = parent; this.titleArea = this.createTitleArea(parent); this.contentArea = this.createContentArea(parent); - this.statusArea = this.createStatusArea(parent); - this.partLayout = new PartLayout(this.parent, this.titleArea, this.contentArea, this.statusArea); + this.partLayout = new PartLayout(this.parent, this.options, this.titleArea, this.contentArea); } /** @@ -68,14 +70,7 @@ export abstract class Part extends WorkbenchComponent { } /** - * Subclasses override to provide a status area implementation. - */ - protected createStatusArea(parent: Builder): Builder { - return null; - } - - /** - * Layout title, content and status area in the given dimension. + * Layout title and content area in the given dimension. */ public layout(dimension: Dimension): Dimension[] { return this.partLayout.layout(dimension); @@ -89,100 +84,32 @@ export abstract class Part extends WorkbenchComponent { } } -export class EmptyPart extends Part { - constructor(id: string) { - super(id); - } -} - -interface IContainerStyle { - borderLeftWidth: number; - borderRightWidth: number; - borderTopWidth: number; - borderBottomWidth: number; -} - -interface ITitleStatusStyle { - display: string; - height: number; -} +const TITLE_HEIGHT = 35; export class PartLayout { - private container: Builder; - private titleArea: Builder; - private contentArea: Builder; - private statusArea: Builder; - private titleStyle: ITitleStatusStyle; - private containerStyle: IContainerStyle; - private statusStyle: ITitleStatusStyle; - constructor(container: Builder, titleArea: Builder, contentArea: Builder, statusArea: Builder) { - this.container = container; - this.titleArea = titleArea; - this.contentArea = contentArea; - this.statusArea = statusArea; - } - - public computeStyle(): void { - const containerStyle = this.container.getComputedStyle(); - this.containerStyle = { - borderLeftWidth: parseInt(containerStyle.getPropertyValue('border-left-width'), 10), - borderRightWidth: parseInt(containerStyle.getPropertyValue('border-right-width'), 10), - borderTopWidth: parseInt(containerStyle.getPropertyValue('border-top-width'), 10), - borderBottomWidth: parseInt(containerStyle.getPropertyValue('border-bottom-width'), 10) - }; - - if (this.titleArea) { - const titleStyle = this.titleArea.getComputedStyle(); - this.titleStyle = { - display: titleStyle.getPropertyValue('display'), - height: this.titleArea.getTotalSize().height - }; - } - - if (this.statusArea) { - const statusStyle = this.statusArea.getComputedStyle(); - this.statusStyle = { - display: statusStyle.getPropertyValue('display'), - height: this.statusArea.getTotalSize().height - }; - } + constructor(private container: Builder, private options: IPartOptions, private titleArea: Builder, private contentArea: Builder) { } public layout(dimension: Dimension): Dimension[] { - if (!this.containerStyle) { - this.computeStyle(); - } + const {width, height} = dimension; - const width = dimension.width - (this.containerStyle.borderLeftWidth + this.containerStyle.borderRightWidth); - const height = dimension.height - (this.containerStyle.borderTopWidth + this.containerStyle.borderBottomWidth); - - // Return the applied sizes to title, content and status + // Return the applied sizes to title and content const sizes: Dimension[] = []; // Title Size: Width (Fill), Height (Variable) let titleSize: Dimension; - if (this.titleArea && this.titleStyle.display !== 'none') { - titleSize = new Dimension(width, Math.min(height, this.titleStyle.height)); + if (this.options && this.options.hasTitle) { + titleSize = new Dimension(width, Math.min(height, TITLE_HEIGHT)); } else { titleSize = new Dimension(0, 0); } - // Status Size: Width (Fill), Height (Variable) - let statusSize: Dimension; - if (this.statusArea && this.statusStyle.display !== 'none') { - this.statusArea.getHTMLElement().style.height = this.statusArea.getHTMLElement().style.width = ''; - statusSize = new Dimension(width, Math.min(height - titleSize.height, this.statusStyle.height)); - } else { - statusSize = new Dimension(0, 0); - } - // Content Size: Width (Fill), Height (Variable) - const contentSize = new Dimension(width, height - titleSize.height - statusSize.height); + const contentSize = new Dimension(width, height - titleSize.height); sizes.push(titleSize); sizes.push(contentSize); - sizes.push(statusSize); // Content if (this.contentArea) { diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts b/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts index 7cee0fd511a..ba253f8beee 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts @@ -8,7 +8,6 @@ import 'vs/css!./media/activityaction'; import nls = require('vs/nls'); import DOM = require('vs/base/browser/dom'); -import errors = require('vs/base/common/errors'); import { TPromise } from 'vs/base/common/winjs.base'; import { Builder, $ } from 'vs/base/browser/builder'; import { DelayedDragHandler } from 'vs/base/browser/dnd'; @@ -93,13 +92,11 @@ export class ViewletActivityAction extends ActivityAction { // Hide sidebar if selected viewlet already visible if (sideBarVisible && activeViewlet && activeViewlet.getId() === this.viewlet.id) { - this.partService.setSideBarHidden(true); - } else { - this.viewletService.openViewlet(this.viewlet.id, true).done(null, errors.onUnexpectedError); - this.activate(); + return this.partService.setSideBarHidden(true); } - return TPromise.as(true); + return this.viewletService.openViewlet(this.viewlet.id, true) + .then(() => this.activate()); } } @@ -531,12 +528,10 @@ class OpenViewletAction extends Action { // Hide sidebar if selected viewlet already visible if (sideBarVisible && activeViewlet && activeViewlet.getId() === this.viewlet.id) { - this.partService.setSideBarHidden(true); - } else { - this.viewletService.openViewlet(this.viewlet.id, true).done(null, errors.onUnexpectedError); + return this.partService.setSideBarHidden(true); } - return TPromise.as(true); + return this.viewletService.openViewlet(this.viewlet.id, true); } } diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts index fe855b35e7f..0129517d71c 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts @@ -64,7 +64,7 @@ export class ActivitybarPart extends Part implements IActivityBarService { @IInstantiationService private instantiationService: IInstantiationService, @IPartService private partService: IPartService ) { - super(id); + super(id, { hasTitle: false }); this.viewletIdToActionItems = Object.create(null); this.viewletIdToActions = Object.create(null); @@ -350,7 +350,7 @@ export class ActivitybarPart extends Part implements IActivityBarService { // Case: we closed the last visible viewlet // Solv: we hide the sidebar else if (visibleViewlets.length === 1) { - unpinPromise = TPromise.as(this.partService.setSideBarHidden(true)); + unpinPromise = this.partService.setSideBarHidden(true); } // Case: we closed the default viewlet diff --git a/src/vs/workbench/browser/parts/compositePart.ts b/src/vs/workbench/browser/parts/compositePart.ts index 044e06f98c2..f8e3f8211af 100644 --- a/src/vs/workbench/browser/parts/compositePart.ts +++ b/src/vs/workbench/browser/parts/compositePart.ts @@ -20,7 +20,7 @@ import { IActionItem, ActionsOrientation } from 'vs/base/browser/ui/actionbar/ac import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar'; import { IActionBarRegistry, Extensions, prepareActions } from 'vs/workbench/browser/actionBarRegistry'; import { Action, IAction } from 'vs/base/common/actions'; -import { Part } from 'vs/workbench/browser/part'; +import { Part, IPartOptions } from 'vs/workbench/browser/part'; import { Composite, CompositeRegistry } from 'vs/workbench/browser/composite'; import { IComposite } from 'vs/workbench/common/composite'; import { WorkbenchProgressService } from 'vs/workbench/services/progress/browser/progressService'; @@ -65,9 +65,10 @@ export abstract class CompositePart extends Part { private nameForTelemetry: string, private compositeCSSClass: string, private actionContributionScope: string, - id: string + id: string, + options: IPartOptions ) { - super(id); + super(id, options); this.instantiatedCompositeListeners = []; this.mapCompositeToCompositeContainer = {}; diff --git a/src/vs/workbench/browser/parts/editor/baseEditor.ts b/src/vs/workbench/browser/parts/editor/baseEditor.ts index 1b584afd8df..35fd51b7004 100644 --- a/src/vs/workbench/browser/parts/editor/baseEditor.ts +++ b/src/vs/workbench/browser/parts/editor/baseEditor.ts @@ -83,7 +83,7 @@ export abstract class BaseEditor extends Panel implements IEditor { /** * Called to create the editor in the parent builder. */ - public abstract createEditor(parent: Builder): void; + protected abstract createEditor(parent: Builder): void; /** * Overload this function to allow for passing in a position argument. @@ -99,7 +99,7 @@ export abstract class BaseEditor extends Panel implements IEditor { return promise; } - public setEditorVisible(visible: boolean, position: Position = null): void { + protected setEditorVisible(visible: boolean, position: Position = null): void { this._position = position; } diff --git a/src/vs/workbench/browser/parts/editor/binaryDiffEditor.ts b/src/vs/workbench/browser/parts/editor/binaryDiffEditor.ts index fef79c08e73..2a257d446f2 100644 --- a/src/vs/workbench/browser/parts/editor/binaryDiffEditor.ts +++ b/src/vs/workbench/browser/parts/editor/binaryDiffEditor.ts @@ -59,7 +59,7 @@ export class BinaryResourceDiffEditor extends BaseEditor implements IVerticalSas return this.input ? this.input.getName() : nls.localize('binaryDiffEditor', "Binary Diff Viewer"); } - public createEditor(parent: Builder): void { + protected createEditor(parent: Builder): void { // Left Container for Binary const leftBinaryContainerElement = document.createElement('div'); diff --git a/src/vs/workbench/browser/parts/editor/binaryEditor.ts b/src/vs/workbench/browser/parts/editor/binaryEditor.ts index e28966bcdeb..2d319de8156 100644 --- a/src/vs/workbench/browser/parts/editor/binaryEditor.ts +++ b/src/vs/workbench/browser/parts/editor/binaryEditor.ts @@ -44,7 +44,7 @@ export abstract class BaseBinaryResourceEditor extends BaseEditor { return this.input ? this.input.getName() : nls.localize('binaryEditor', "Binary Viewer"); } - public createEditor(parent: Builder): void { + protected createEditor(parent: Builder): void { // Container for Binary const binaryContainerElement = document.createElement('div'); diff --git a/src/vs/workbench/browser/parts/editor/editorActions.ts b/src/vs/workbench/browser/parts/editor/editorActions.ts index e70751ab74f..7086cc67ca2 100644 --- a/src/vs/workbench/browser/parts/editor/editorActions.ts +++ b/src/vs/workbench/browser/parts/editor/editorActions.ts @@ -824,7 +824,7 @@ export class MaximizeGroupAction extends Action { public run(): TPromise { if (this.editorService.getActiveEditor()) { this.editorGroupService.arrangeGroups(GroupArrangement.MINIMIZE_OTHERS); - this.partService.setSideBarHidden(true); + return this.partService.setSideBarHidden(true); } return TPromise.as(false); diff --git a/src/vs/workbench/browser/parts/editor/editorPart.ts b/src/vs/workbench/browser/parts/editor/editorPart.ts index 162ea2b23e7..dae95332fd1 100644 --- a/src/vs/workbench/browser/parts/editor/editorPart.ts +++ b/src/vs/workbench/browser/parts/editor/editorPart.ts @@ -115,7 +115,7 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupService @IContextKeyService contextKeyService: IContextKeyService, @IInstantiationService private instantiationService: IInstantiationService ) { - super(id); + super(id, { hasTitle: false }); this._onEditorsChanged = new Emitter(); this._onEditorsMoved = new Emitter(); diff --git a/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts b/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts index 45f8f19f20b..63b5dd54eed 100644 --- a/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts +++ b/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts @@ -39,7 +39,7 @@ export class SideBySideEditor extends BaseEditor { super(SideBySideEditor.ID, telemetryService); } - public createEditor(parent: Builder): void { + protected createEditor(parent: Builder): void { const parentElement = parent.getHTMLElement(); DOM.addClass(parentElement, 'side-by-side-editor'); this.createSash(parentElement); @@ -51,7 +51,7 @@ export class SideBySideEditor extends BaseEditor { .then(() => this.updateInput(oldInput, newInput, options)); } - public setEditorVisible(visible: boolean, position: Position): void { + protected setEditorVisible(visible: boolean, position: Position): void { if (this.masterEditor) { this.masterEditor.setVisible(visible, position); } diff --git a/src/vs/workbench/browser/parts/editor/textDiffEditor.ts b/src/vs/workbench/browser/parts/editor/textDiffEditor.ts index 621985ff420..9fc75180eca 100644 --- a/src/vs/workbench/browser/parts/editor/textDiffEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textDiffEditor.ts @@ -8,13 +8,14 @@ import 'vs/css!./media/textdiffeditor'; import { TPromise } from 'vs/base/common/winjs.base'; import nls = require('vs/nls'); +import objects = require('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 types = require('vs/base/common/types'); import { IDiffEditor } from 'vs/editor/browser/editorBrowser'; import { IDiffEditorOptions, IEditorOptions } from 'vs/editor/common/editorCommon'; -import { BaseTextEditor } from 'vs/workbench/browser/parts/editor/textEditor'; +import { BaseTextEditor, IEditorConfiguration } from 'vs/workbench/browser/parts/editor/textEditor'; import { TextEditorOptions, TextDiffEditorOptions, EditorModel, EditorInput, EditorOptions, TEXT_DIFF_EDITOR_ID } from 'vs/workbench/common/editor'; import { StringEditorInput } from 'vs/workbench/common/editor/stringEditorInput'; import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; @@ -66,7 +67,7 @@ export class TextDiffEditor extends BaseTextEditor { return nls.localize('textDiffEditor', "Text Diff Editor"); } - public createEditorControl(parent: Builder): IDiffEditor { + public createEditorControl(parent: Builder, configuration: IEditorOptions): IDiffEditor { // Actions this.nextDiffAction = new NavigateAction(this, true); @@ -103,7 +104,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(), this.getCodeEditorOptions()); + return diffEditorInstantiator.createInstance(DiffEditorWidget, parent.getHTMLElement(), configuration); } public setInput(input: EditorInput, options?: EditorOptions): TPromise { @@ -167,9 +168,6 @@ export class TextDiffEditor extends BaseTextEditor { if (options && types.isFunction((options).apply)) { (options).apply(diffEditor); } - - // Apply options again because input has changed - diffEditor.updateOptions(this.getCodeEditorOptions()); }, (error) => { // In case we tried to open a file and the response indicates that this is not a text file, fallback to binary diff. @@ -197,8 +195,19 @@ export class TextDiffEditor extends BaseTextEditor { return false; } - protected getCodeEditorOptions(): IEditorOptions { - const options: IDiffEditorOptions = super.getCodeEditorOptions(); + protected computeConfiguration(configuration: IEditorConfiguration): IEditorOptions { + const editorConfiguration = super.computeConfiguration(configuration); + + // Handle diff editor specially by merging in diffEditor configuration + if (types.isObject(configuration.diffEditor)) { + objects.mixin(editorConfiguration, configuration.diffEditor); + } + + return editorConfiguration; + } + + protected getConfigurationOverrides(): IEditorOptions { + const options: IDiffEditorOptions = super.getConfigurationOverrides(); const input = this.input; if (input instanceof DiffEditorInput) { diff --git a/src/vs/workbench/browser/parts/editor/textEditor.ts b/src/vs/workbench/browser/parts/editor/textEditor.ts index 18a16159854..090500a91c1 100644 --- a/src/vs/workbench/browser/parts/editor/textEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textEditor.ts @@ -14,7 +14,7 @@ import DOM = require('vs/base/browser/dom'); import { CodeEditor } from 'vs/editor/browser/codeEditor'; import { EditorInput, EditorOptions } from 'vs/workbench/common/editor'; import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; -import { IEditorViewState, IEditor, IEditorOptions, EventType as EditorEventType, EditorType } from 'vs/editor/common/editorCommon'; +import { IEditorViewState, IEditor, IEditorOptions, EventType as EditorEventType } from 'vs/editor/common/editorCommon'; import { Position } from 'vs/platform/editor/common/editor'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -33,7 +33,7 @@ interface ITextEditorViewState { 2?: IEditorViewState; } -interface IEditorConfiguration { +export interface IEditorConfiguration { editor: any; diffEditor: any; } @@ -87,20 +87,22 @@ export abstract class BaseTextEditor extends BaseEditor { return; } - // Specific editor options always overwrite user configuration - const editorConfiguration = types.isObject(configuration.editor) ? objects.clone(configuration.editor) : Object.create(null); - objects.assign(editorConfiguration, this.getCodeEditorOptions()); - - // Handle diff editor specially by merging in diffEditor configuration - if (this.editorControl.getEditorType() === EditorType.IDiffEditor && types.isObject(configuration.diffEditor)) { - objects.mixin(editorConfiguration, configuration.diffEditor); - } + const editorConfiguration = this.computeConfiguration(configuration); // Apply to control this.editorControl.updateOptions(editorConfiguration); } - protected getCodeEditorOptions(): IEditorOptions { + protected computeConfiguration(configuration: IEditorConfiguration): IEditorOptions { + + // Specific editor options always overwrite user configuration + const editorConfiguration = types.isObject(configuration.editor) ? objects.clone(configuration.editor) : Object.create(null); + objects.assign(editorConfiguration, this.getConfigurationOverrides()); + + return editorConfiguration; + } + + protected getConfigurationOverrides(): IEditorOptions { return { overviewRulerLanes: 3, lineNumbersMinChars: 3, @@ -109,20 +111,29 @@ export abstract class BaseTextEditor extends BaseEditor { }; } - public createEditor(parent: Builder): void { + protected createEditor(parent: Builder): void { // Editor for Text this._editorContainer = parent; - this.editorControl = this.createEditorControl(parent); + this.editorControl = this.createEditorControl(parent, this.computeConfiguration(this.configurationService.getConfiguration())); // Application & Editor focus change if (this.editorControl instanceof EventEmitter) { this.toUnbind.push(this.editorControl.addListener2(EditorEventType.EditorBlur, () => this.onEditorFocusLost())); } this.toUnbind.push(DOM.addDisposableListener(window, DOM.EventType.BLUR, () => this.onWindowFocusLost())); + } - // Configuration - this.applyConfiguration(this.configurationService.getConfiguration()); + /** + * This method creates and returns the text editor control to be used. Subclasses can override to + * provide their own editor control that should be used (e.g. a DiffEditor). + * + * The passed in configuration object should be passed to the editor control when creating it. + */ + protected createEditorControl(parent: Builder, 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); } private onEditorFocusLost(): void { @@ -158,23 +169,16 @@ export abstract class BaseTextEditor extends BaseEditor { }); } - /** - * This method creates and returns the text editor control to be used. Subclasses can override to - * provide their own editor control that should be used (e.g. a DiffEditor). - */ - public createEditorControl(parent: Builder): IEditor { - - // Use a getter for the instantiation service since some subclasses might use scoped instantiation services - return this.instantiationService.createInstance(CodeEditor, parent.getHTMLElement(), this.getCodeEditorOptions()); - } - public setInput(input: EditorInput, options?: EditorOptions): TPromise { return super.setInput(input, options).then(() => { - this.editorControl.updateOptions(this.getCodeEditorOptions()); // support input specific editor options + + // 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.editorControl.updateOptions(this.getConfigurationOverrides()); }); } - public setEditorVisible(visible: boolean, position: Position = null): void { + protected setEditorVisible(visible: boolean, position: Position = null): void { // Pass on to Editor if (visible) { diff --git a/src/vs/workbench/browser/parts/editor/textResourceEditor.ts b/src/vs/workbench/browser/parts/editor/textResourceEditor.ts index 88871b501c4..25299ae5adb 100644 --- a/src/vs/workbench/browser/parts/editor/textResourceEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textResourceEditor.ts @@ -111,9 +111,6 @@ export class TextResourceEditor extends BaseTextEditor { if (!optionsGotApplied) { this.restoreViewState(input); } - - // Apply options again because input has changed - textEditor.updateOptions(this.getCodeEditorOptions()); }); } @@ -126,8 +123,8 @@ export class TextResourceEditor extends BaseTextEditor { } } - protected getCodeEditorOptions(): IEditorOptions { - const options = super.getCodeEditorOptions(); + protected getConfigurationOverrides(): IEditorOptions { + const options = super.getConfigurationOverrides(); const input = this.input; const isUntitled = input instanceof UntitledEditorInput; diff --git a/src/vs/workbench/browser/parts/panel/panelPart.ts b/src/vs/workbench/browser/parts/panel/panelPart.ts index 5d0395fd3f5..f3e7b5973f8 100644 --- a/src/vs/workbench/browser/parts/panel/panelPart.ts +++ b/src/vs/workbench/browser/parts/panel/panelPart.ts @@ -58,7 +58,8 @@ export class PanelPart extends CompositePart implements IPanelService { 'panel', 'panel', Scope.PANEL, - id + id, + { hasTitle: true } ); } @@ -80,16 +81,17 @@ export class PanelPart extends CompositePart implements IPanelService { } // First check if panel is hidden and show if so + let promise = TPromise.as(null); if (!this.partService.isVisible(Parts.PANEL_PART)) { try { this.blockOpeningPanel = true; - this.partService.setPanelHidden(false); + promise = this.partService.setPanelHidden(false); } finally { this.blockOpeningPanel = false; } } - return this.openComposite(id, focus); + return promise.then(() => this.openComposite(id, focus)); } protected getActions(): IAction[] { @@ -122,9 +124,8 @@ class ClosePanelAction extends Action { super(id, name, 'hide-panel-action'); } - public run(): TPromise { - this.partService.setPanelHidden(true); - return TPromise.as(true); + public run(): TPromise { + return this.partService.setPanelHidden(true); } } @@ -140,9 +141,8 @@ export class TogglePanelAction extends ActivityAction { super(id, name, partService.isVisible(Parts.PANEL_PART) ? 'panel expanded' : 'panel'); } - public run(): TPromise { - this.partService.setPanelHidden(this.partService.isVisible(Parts.PANEL_PART)); - return TPromise.as(true); + public run(): TPromise { + return this.partService.setPanelHidden(this.partService.isVisible(Parts.PANEL_PART)); } } @@ -160,21 +160,18 @@ class FocusPanelAction extends Action { super(id, label); } - public run(): TPromise { + public run(): TPromise { // Show panel if (!this.partService.isVisible(Parts.PANEL_PART)) { - this.partService.setPanelHidden(false); + return this.partService.setPanelHidden(false); } // Focus into active panel - else { - let panel = this.panelService.getActivePanel(); - if (panel) { - panel.focus(); - } + let panel = this.panelService.getActivePanel(); + if (panel) { + panel.focus(); } - return TPromise.as(true); } } @@ -192,12 +189,10 @@ class ToggleMaximizedPanelAction extends Action { super(id, label); } - public run(): TPromise { + public run(): TPromise { // Show panel - this.partService.setPanelHidden(false); - this.partService.toggleMaximizedPanel(); - - return TPromise.as(true); + return this.partService.setPanelHidden(false) + .then(() => this.partService.toggleMaximizedPanel()); } } diff --git a/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts b/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts index c381a4b44c2..cdbf0867f75 100644 --- a/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts +++ b/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts @@ -65,7 +65,8 @@ export class SidebarPart extends CompositePart implements ISidebar { 'sideBar', 'viewlet', Scope.VIEWLET, - id + id, + { hasTitle: true } ); } @@ -83,16 +84,17 @@ export class SidebarPart extends CompositePart implements ISidebar { } // First check if sidebar is hidden and show if so + let promise = TPromise.as(null); if (!this.partService.isVisible(Parts.SIDEBAR_PART)) { try { this.blockOpeningViewlet = true; - this.partService.setSideBarHidden(false); + promise = this.partService.setSideBarHidden(false); } finally { this.blockOpeningViewlet = false; } } - return this.openComposite(id, focus); + return promise.then(() => this.openComposite(id, focus)); } public getActiveViewlet(): IViewlet { @@ -122,21 +124,18 @@ class FocusSideBarAction extends Action { super(id, label); } - public run(): TPromise { + public run(): TPromise { // Show side bar if (!this.partService.isVisible(Parts.SIDEBAR_PART)) { - this.partService.setSideBarHidden(false); + return this.partService.setSideBarHidden(false); } // Focus into active viewlet - else { - let viewlet = this.viewletService.getActiveViewlet(); - if (viewlet) { - viewlet.focus(); - } + let viewlet = this.viewletService.getActiveViewlet(); + if (viewlet) { + viewlet.focus(); } - return TPromise.as(true); } } diff --git a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts index ac76450fd17..54ad8b61c3b 100644 --- a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts +++ b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts @@ -42,7 +42,7 @@ export class StatusbarPart extends Part implements IStatusbarService { id: string, @IInstantiationService private instantiationService: IInstantiationService ) { - super(id); + super(id, { hasTitle: false }); this.toDispose = []; } diff --git a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts index f758d84fa1b..ac2659b74a9 100644 --- a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts +++ b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts @@ -35,7 +35,7 @@ export class TitlebarPart extends Part implements ITitleService { @IWindowService private windowService: IWindowService, @IWindowsService private windowsService: IWindowsService ) { - super(id); + super(id, { hasTitle: false }); this.registerListeners(); } diff --git a/src/vs/workbench/electron-browser/workbench.ts b/src/vs/workbench/electron-browser/workbench.ts index ff5e2429ae5..f272385d250 100644 --- a/src/vs/workbench/electron-browser/workbench.ts +++ b/src/vs/workbench/electron-browser/workbench.ts @@ -662,7 +662,7 @@ export class Workbench implements IPartService { // Layout if (!skipLayout) { - this.workbenchLayout.layout({ forceStyleRecompute: true }); + this.workbenchLayout.layout(); } } @@ -672,11 +672,11 @@ export class Workbench implements IPartService { // Layout if (!skipLayout) { - this.workbenchLayout.layout({ forceStyleRecompute: true }); + this.workbenchLayout.layout(); } } - public setSideBarHidden(hidden: boolean, skipLayout?: boolean): void { + public setSideBarHidden(hidden: boolean, skipLayout?: boolean): TPromise { this.sideBarHidden = hidden; // Adjust CSS @@ -686,39 +686,42 @@ export class Workbench implements IPartService { this.workbench.removeClass('nosidebar'); } - // Layout - if (!skipLayout) { - this.workbenchLayout.layout({ forceStyleRecompute: true }); - } - + let promise = TPromise.as(null); // If sidebar becomes hidden, also hide the current active Viewlet if any if (hidden && this.sidebarPart.getActiveViewlet()) { - this.sidebarPart.hideActiveViewlet(); + promise = this.sidebarPart.hideActiveViewlet().then(() => { + const activeEditor = this.editorPart.getActiveEditor(); + const activePanel = this.panelPart.getActivePanel(); - const activeEditor = this.editorPart.getActiveEditor(); - const activePanel = this.panelPart.getActivePanel(); - - // Pass Focus to Editor or Panel if Sidebar is now hidden - if (this.hasFocus(Parts.PANEL_PART) && activePanel) { - activePanel.focus(); - } else if (activeEditor) { - activeEditor.focus(); - } + // Pass Focus to Editor or Panel if Sidebar is now hidden + if (this.hasFocus(Parts.PANEL_PART) && activePanel) { + activePanel.focus(); + } else if (activeEditor) { + activeEditor.focus(); + } + }); } // If sidebar becomes visible, show last active Viewlet or default viewlet else if (!hidden && !this.sidebarPart.getActiveViewlet()) { const viewletToOpen = this.sidebarPart.getLastActiveViewletId() || this.viewletService.getDefaultViewletId(); if (viewletToOpen) { - this.sidebarPart.openViewlet(viewletToOpen, true).done(null, errors.onUnexpectedError); + promise = this.sidebarPart.openViewlet(viewletToOpen, true); } } - // Remember in settings - this.storageService.store(Workbench.sidebarHiddenSettingKey, hidden ? 'true' : 'false', StorageScope.WORKSPACE); + return promise.then(() => { + // Remember in settings + this.storageService.store(Workbench.sidebarHiddenSettingKey, hidden ? 'true' : 'false', StorageScope.WORKSPACE); + + // Layout + if (!skipLayout) { + this.workbenchLayout.layout(); + } + }); } - public setPanelHidden(hidden: boolean, skipLayout?: boolean): void { + public setPanelHidden(hidden: boolean, skipLayout?: boolean): TPromise { this.panelHidden = hidden; // Adjust CSS @@ -728,20 +731,16 @@ export class Workbench implements IPartService { this.workbench.removeClass('nopanel'); } - // Layout - if (!skipLayout) { - this.workbenchLayout.layout({ forceStyleRecompute: true }); - } - + let promise = TPromise.as(null); // If panel part becomes hidden, also hide the current active panel if any if (hidden && this.panelPart.getActivePanel()) { - this.panelPart.hideActivePanel(); - - // Pass Focus to Editor if Panel part is now hidden - const editor = this.editorPart.getActiveEditor(); - if (editor) { - editor.focus(); - } + promise = this.panelPart.hideActivePanel().then(() => { + // Pass Focus to Editor if Panel part is now hidden + const editor = this.editorPart.getActiveEditor(); + if (editor) { + editor.focus(); + } + }); } // If panel part becomes visible, show last active panel or default panel @@ -749,16 +748,23 @@ export class Workbench implements IPartService { const registry = Registry.as(PanelExtensions.Panels); const panelToOpen = this.panelPart.getLastActivePanelId() || registry.getDefaultPanelId(); if (panelToOpen) { - this.panelPart.openPanel(panelToOpen, true).done(null, errors.onUnexpectedError); + promise = this.panelPart.openPanel(panelToOpen, true); } } - // Remember in settings - this.storageService.store(Workbench.panelHiddenSettingKey, hidden ? 'true' : 'false', StorageScope.WORKSPACE); + return promise.then(() => { + // Remember in settings + this.storageService.store(Workbench.panelHiddenSettingKey, hidden ? 'true' : 'false', StorageScope.WORKSPACE); + + // Layout + if (!skipLayout) { + this.workbenchLayout.layout(); + } + }); } public toggleMaximizedPanel(): void { - this.workbenchLayout.layout({ forceStyleRecompute: true, toggleMaximizedPanel: true }); + this.workbenchLayout.layout({ toggleMaximizedPanel: true }); } public getSideBarPosition(): Position { @@ -767,7 +773,7 @@ export class Workbench implements IPartService { private setSideBarPosition(position: Position): void { if (this.sideBarHidden) { - this.setSideBarHidden(false, true /* Skip Layout */); + this.setSideBarHidden(false, true /* Skip Layout */).done(undefined, errors.onUnexpectedError); } const newPositionValue = (position === Position.LEFT) ? 'left' : 'right'; @@ -781,7 +787,7 @@ export class Workbench implements IPartService { this.sidebarPart.getContainer().addClass(newPositionValue); // Layout - this.workbenchLayout.layout({ forceStyleRecompute: true }); + this.workbenchLayout.layout(); } public dispose(): void { @@ -1072,8 +1078,8 @@ export class Workbench implements IPartService { this.zenMode.transitionedToFullScreen = toggleFullScreen; this.zenMode.wasSideBarVisible = this.isVisible(Parts.SIDEBAR_PART); this.zenMode.wasPanelVisible = this.isVisible(Parts.PANEL_PART); - this.setPanelHidden(true, true); - this.setSideBarHidden(true, true); + this.setPanelHidden(true, true).done(undefined, errors.onUnexpectedError); + this.setSideBarHidden(true, true).done(undefined, errors.onUnexpectedError); this.setActivityBarHidden(true, true); if (config.hideStatusBar) { @@ -1084,10 +1090,10 @@ export class Workbench implements IPartService { } } else { if (this.zenMode.wasPanelVisible) { - this.setPanelHidden(false, true); + this.setPanelHidden(false, true).done(undefined, errors.onUnexpectedError); } if (this.zenMode.wasSideBarVisible) { - this.setSideBarHidden(false, true); + this.setSideBarHidden(false, true).done(undefined, errors.onUnexpectedError); } // Status bar and activity bar visibility come from settings -> update their visibility. this.onDidUpdateConfiguration(true); diff --git a/src/vs/workbench/parts/debug/common/debugProtocol.d.ts b/src/vs/workbench/parts/debug/common/debugProtocol.d.ts index 21c69e61090..ac848f1f5a0 100644 --- a/src/vs/workbench/parts/debug/common/debugProtocol.d.ts +++ b/src/vs/workbench/parts/debug/common/debugProtocol.d.ts @@ -1048,6 +1048,8 @@ declare module DebugProtocol { path?: string; /** If sourceReference > 0 the contents of the source must be retrieved through the SourceRequest (even if a path is specified). A sourceReference is only valid for a session, so it must not be used to persist a source. */ sourceReference?: number; + /** An optional hint for how to present the source in the UI. A value of 'deemphasize' can be used to indicate that the source is not available or that it is skipped on stepping. */ + presentationHint?: 'emphasize' | 'deemphasize'; /** The (optional) origin of this source: possible values 'internal module', 'inlined content from source map', etc. */ origin?: string; /** Optional data that a debug adapter might want to loop through the client. The client should leave the data intact and persist it across sessions. The client should not interpret the data. */ diff --git a/src/vs/workbench/parts/debug/electron-browser/debugConfigurationManager.ts b/src/vs/workbench/parts/debug/electron-browser/debugConfigurationManager.ts index e58380990f8..539ed2b4206 100644 --- a/src/vs/workbench/parts/debug/electron-browser/debugConfigurationManager.ts +++ b/src/vs/workbench/parts/debug/electron-browser/debugConfigurationManager.ts @@ -333,7 +333,7 @@ export class ConfigurationManager implements debug.IConfigurationManager { let configFileCreated = false; return this.fileService.resolveContent(resource).then(content => true, err => - this.quickOpenService.pick([...this.adapters, { label: 'More...' }], { placeHolder: nls.localize('selectDebug', "Select Environment") }) + this.quickOpenService.pick([...this.adapters.filter(a => a.hasInitialConfiguration()), { label: 'More...' }], { placeHolder: nls.localize('selectDebug', "Select Environment") }) .then(picked => { if (picked instanceof Adapter) { return picked ? picked.getInitialConfigurationContent() : null; diff --git a/src/vs/workbench/parts/debug/electron-browser/debugEditorContribution.ts b/src/vs/workbench/parts/debug/electron-browser/debugEditorContribution.ts index 4fd7b01fa9d..074af2646d1 100644 --- a/src/vs/workbench/parts/debug/electron-browser/debugEditorContribution.ts +++ b/src/vs/workbench/parts/debug/electron-browser/debugEditorContribution.ts @@ -274,13 +274,13 @@ export class DebugEditorContribution implements IDebugEditorContribution { if (model && LAUNCH_JSON_REGEX.test(model.uri.toString())) { this.configurationWidget = this.instantiationService.createInstance(FloatingClickWidget, this.editor, nls.localize('addConfiguration', "Add Configuration"), null); this.configurationWidget.render(); - this.toDispose.push(this.configurationWidget.onClick(() => this.addLaunchConfiguration())); + this.toDispose.push(this.configurationWidget.onClick(() => this.addLaunchConfiguration().done(undefined, errors.onUnexpectedError))); } else if (this.configurationWidget) { this.configurationWidget.dispose(); } } - private addLaunchConfiguration(): void { + public addLaunchConfiguration(): TPromise { let configurationsPosition: IPosition; const model = this.editor.getModel(); let depthInArray = 0; @@ -315,7 +315,7 @@ export class DebugEditorContribution implements IDebugEditorContribution { return this.commandService.executeCommand('editor.action.insertLineAfter'); }; - insertLineAfter(configurationsPosition.lineNumber).done(() => this.commandService.executeCommand('editor.action.triggerSuggest'), errors.onUnexpectedError); + return insertLineAfter(configurationsPosition.lineNumber).then(() => this.commandService.executeCommand('editor.action.triggerSuggest')); } private static BREAKPOINT_HELPER_DECORATION: IModelDecorationOptions = { diff --git a/src/vs/workbench/parts/debug/node/debugAdapter.ts b/src/vs/workbench/parts/debug/node/debugAdapter.ts index 166d7612deb..13bef462821 100644 --- a/src/vs/workbench/parts/debug/node/debugAdapter.ts +++ b/src/vs/workbench/parts/debug/node/debugAdapter.ts @@ -84,6 +84,10 @@ export class Adapter { objects.mixin(this.rawAdapter, secondRawAdapter, extensionDescription.isBuiltin); } + public hasInitialConfiguration(): boolean { + return !!this.rawAdapter.initialConfigurations; + } + public getInitialConfigurationContent(): TPromise { const editorConfig = this.configurationService.getConfiguration(); if (typeof this.rawAdapter.initialConfigurations === 'string') { diff --git a/src/vs/workbench/parts/extensions/node/extensionsWorkbenchService.ts b/src/vs/workbench/parts/extensions/node/extensionsWorkbenchService.ts index 64e21cd4d8d..7dc3eb0d8a6 100644 --- a/src/vs/workbench/parts/extensions/node/extensionsWorkbenchService.ts +++ b/src/vs/workbench/parts/extensions/node/extensionsWorkbenchService.ts @@ -500,7 +500,7 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService { return TPromise.wrap(null); } - return this.doSetEnablement(extension, enable, workspace).then(reload => { + return this.promptAndSetEnablement(extension, enable, workspace).then(reload => { this.telemetryService.publicLog(enable ? 'extension:enable' : 'extension:disable', extension.telemetryData); }); } @@ -521,6 +521,120 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService { } + private promptAndSetEnablement(extension: IExtension, enable: boolean, workspace: boolean): TPromise { + const allDependencies = this.getDependenciesRecursively(extension, this.local, enable, workspace, []); + if (allDependencies.length > 0) { + if (enable) { + return this.promptForDependenciesAndEnable(extension, allDependencies, workspace); + } else { + return this.promptForDependenciesAndDisable(extension, allDependencies, workspace); + } + } + return this.checkAndSetEnablement(extension, [], enable, workspace); + } + + private promptForDependenciesAndEnable(extension: IExtension, dependencies: IExtension[], workspace: boolean): TPromise { + const message = nls.localize('enableDependeciesConfirmation', "Enabling '{0}' also enable its dependencies. Would you like to continue?", extension.displayName); + const options = [ + nls.localize('enable', "Yes"), + nls.localize('doNotEnable', "No") + ]; + return this.choiceService.choose(Severity.Info, message, options, true) + .then(value => { + if (value === 0) { + return this.checkAndSetEnablement(extension, dependencies, true, workspace); + } + return TPromise.as(null); + }); + } + + private promptForDependenciesAndDisable(extension: IExtension, dependencies: IExtension[], workspace: boolean): TPromise { + const message = nls.localize('disableDependeciesConfirmation', "Would you like to disable '{0}' only or its dependencies also?", extension.displayName); + const options = [ + nls.localize('disableOnly', "Only"), + nls.localize('disableAll', "All"), + nls.localize('cancel', "Cancel") + ]; + return this.choiceService.choose(Severity.Info, message, options, true) + .then(value => { + if (value === 0) { + return this.checkAndSetEnablement(extension, [], false, workspace); + } + if (value === 1) { + return this.checkAndSetEnablement(extension, dependencies, false, workspace); + } + return TPromise.as(null); + }); + } + + private checkAndSetEnablement(extension: IExtension, dependencies: IExtension[], enable: boolean, workspace: boolean): TPromise { + if (!enable) { + let dependents = this.getDependentsAfterDisablement(extension, dependencies, this.local, workspace); + if (dependents.length) { + return TPromise.wrapError(this.getDependentsErrorMessage(extension, dependents)); + } + } + return TPromise.join([extension, ...dependencies].map(e => this.doSetEnablement(e, enable, workspace))); + } + + private getDependenciesRecursively(extension: IExtension, installed: IExtension[], enable: boolean, workspace: boolean, checked: IExtension[]): IExtension[] { + if (checked.indexOf(extension) !== -1) { + return []; + } + checked.push(extension); + if (!extension.dependencies || extension.dependencies.length === 0) { + return []; + } + const dependenciesToDisable = installed.filter(i => { + // Do not include extensions which are already disabled and request is to disable + if (!enable && (workspace ? i.disabledForWorkspace : i.disabledGlobally)) { + return false; + } + return extension.dependencies.indexOf(i.identifier) !== -1; + }); + const depsOfDeps = []; + for (const dep of dependenciesToDisable) { + depsOfDeps.push(...this.getDependenciesRecursively(dep, installed, enable, workspace, checked)); + } + return [...dependenciesToDisable, ...depsOfDeps]; + } + + private getDependentsAfterDisablement(extension: IExtension, dependencies: IExtension[], installed: IExtension[], workspace: boolean): IExtension[] { + return installed.filter(i => { + if (i.dependencies.length === 0) { + return false; + } + if (i === extension) { + return false; + } + const disabled = workspace ? i.disabledForWorkspace : i.disabledGlobally; + if (disabled) { + return false; + } + if (dependencies.indexOf(i) !== -1) { + return false; + } + return i.dependencies.some(dep => { + if (extension.identifier === dep) { + return true; + } + return dependencies.some(d => d.identifier === dep); + }); + }); + } + + private getDependentsErrorMessage(extension: IExtension, dependents: IExtension[]): string { + if (dependents.length === 1) { + return nls.localize('singleDependentError', "Cannot disable extension '{0}'. Extension '{1}' depends on this.", extension.displayName, dependents[0].displayName); + } + if (dependents.length === 2) { + return nls.localize('twoDependentsError', "Cannot disable extension '{0}'. Extensions '{1}' and '{2}' depend on this.", + extension.displayName, dependents[0].displayName, dependents[1].displayName); + } + return nls.localize('multipleDependentsError', "Cannot disable extension '{0}'. Extensions '{1}', '{2}' and others depend on this.", + extension.displayName, dependents[0].displayName, dependents[1].displayName); + } + private doSetEnablement(extension: IExtension, enable: boolean, workspace: boolean): TPromise { if (workspace) { return this.extensionEnablementService.setEnablement(extension.identifier, enable, workspace); diff --git a/src/vs/workbench/parts/extensions/test/electron-browser/extensionsWorkbenchService.test.ts b/src/vs/workbench/parts/extensions/test/electron-browser/extensionsWorkbenchService.test.ts index f69872b645c..7147b524286 100644 --- a/src/vs/workbench/parts/extensions/test/electron-browser/extensionsWorkbenchService.test.ts +++ b/src/vs/workbench/parts/extensions/test/electron-browser/extensionsWorkbenchService.test.ts @@ -28,6 +28,7 @@ import { IPager } from 'vs/base/common/paging'; import { ITelemetryService, NullTelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IWorkspaceContextService, WorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { TestWorkspace } from 'vs/platform/workspace/test/common/testWorkspace'; +import { IChoiceService } from 'vs/platform/message/common/message'; suite('ExtensionsWorkbenchService Test', () => { @@ -63,11 +64,14 @@ suite('ExtensionsWorkbenchService Test', () => { instantiationService.stub(IExtensionTipsService, ExtensionTipsService); instantiationService.stub(IExtensionTipsService, 'getKeymapRecommendations', () => []); + + instantiationService.stub(IChoiceService, { choose: () => null }); }); setup(() => { instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', []); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage()); + instantiationService.stubPromise(IChoiceService, 'choose', 0); (instantiationService.get(IExtensionEnablementService)).reset(); }); @@ -786,6 +790,156 @@ suite('ExtensionsWorkbenchService Test', () => { assert.ok(actual.disabledGlobally); }); + test('test disable extension with dependencies disable only itself', () => { + instantiationService.get(IExtensionEnablementService).setEnablement('pub.a', true); + instantiationService.get(IExtensionEnablementService).setEnablement('pub.b', true); + instantiationService.get(IExtensionEnablementService).setEnablement('pub.c', true); + instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [aLocalExtension('a', { extensionDependencies: ['pub.b'] }), aLocalExtension('b'), aLocalExtension('c')]); + testObject = instantiationService.createInstance(ExtensionsWorkbenchService); + + testObject.setEnablement(testObject.local[0], false); + + assert.ok(testObject.local[0].disabledGlobally); + assert.ok(!testObject.local[1].disabledGlobally); + }); + + test('test disable extension with dependencies disable all', () => { + instantiationService.get(IExtensionEnablementService).setEnablement('pub.a', true); + instantiationService.get(IExtensionEnablementService).setEnablement('pub.b', true); + instantiationService.get(IExtensionEnablementService).setEnablement('pub.c', true); + instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [aLocalExtension('a', { extensionDependencies: ['pub.b'] }), aLocalExtension('b'), aLocalExtension('c')]); + instantiationService.stubPromise(IChoiceService, 'choose', 1); + testObject = instantiationService.createInstance(ExtensionsWorkbenchService); + + testObject.setEnablement(testObject.local[0], false); + + assert.ok(testObject.local[0].disabledGlobally); + assert.ok(testObject.local[1].disabledGlobally); + }); + + test('test disable extension fails if extension is a dependent of other', () => { + instantiationService.get(IExtensionEnablementService).setEnablement('pub.a', true); + instantiationService.get(IExtensionEnablementService).setEnablement('pub.b', true); + instantiationService.get(IExtensionEnablementService).setEnablement('pub.c', true); + instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [aLocalExtension('a', { extensionDependencies: ['pub.b'] }), aLocalExtension('b'), aLocalExtension('c')]); + testObject = instantiationService.createInstance(ExtensionsWorkbenchService); + + return testObject.setEnablement(testObject.local[1], false).then(() => assert.fail('Should fail'), error => assert.ok(true)); + }); + + test('test disable extension does not fail if its dependency is a dependent of other but chosen to disable only itself', () => { + instantiationService.get(IExtensionEnablementService).setEnablement('pub.a', true); + instantiationService.get(IExtensionEnablementService).setEnablement('pub.b', true); + instantiationService.get(IExtensionEnablementService).setEnablement('pub.c', true); + instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [aLocalExtension('a', { extensionDependencies: ['pub.b'] }), aLocalExtension('b'), aLocalExtension('c', { extensionDependencies: ['pub.b'] })]); + + testObject = instantiationService.createInstance(ExtensionsWorkbenchService); + + testObject.setEnablement(testObject.local[0], false); + + assert.ok(testObject.local[0].disabledGlobally); + }); + + test('test disable extension fails if its dependency is a dependent of other', () => { + instantiationService.get(IExtensionEnablementService).setEnablement('pub.a', true); + instantiationService.get(IExtensionEnablementService).setEnablement('pub.b', true); + instantiationService.get(IExtensionEnablementService).setEnablement('pub.c', true); + instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [aLocalExtension('a', { extensionDependencies: ['pub.b'] }), aLocalExtension('b'), aLocalExtension('c', { extensionDependencies: ['pub.b'] })]); + instantiationService.stubPromise(IChoiceService, 'choose', 1); + + testObject = instantiationService.createInstance(ExtensionsWorkbenchService); + + return testObject.setEnablement(testObject.local[0], false).then(() => assert.fail('Should fail'), error => assert.ok(true)); + }); + + test('test disable extension if its dependency is a dependent of other disabled extension', () => { + instantiationService.get(IExtensionEnablementService).setEnablement('pub.a', true); + instantiationService.get(IExtensionEnablementService).setEnablement('pub.b', true); + instantiationService.get(IExtensionEnablementService).setEnablement('pub.c', false); + instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [aLocalExtension('a', { extensionDependencies: ['pub.b'] }), aLocalExtension('b'), aLocalExtension('c', { extensionDependencies: ['pub.b'] })]); + instantiationService.stubPromise(IChoiceService, 'choose', 1); + + testObject = instantiationService.createInstance(ExtensionsWorkbenchService); + + testObject.setEnablement(testObject.local[0], false); + + assert.ok(testObject.local[0].disabledGlobally); + assert.ok(testObject.local[1].disabledGlobally); + }); + + test('test disable extension if its dependencys dependency is itself', () => { + instantiationService.get(IExtensionEnablementService).setEnablement('pub.a', true); + instantiationService.get(IExtensionEnablementService).setEnablement('pub.b', true); + instantiationService.get(IExtensionEnablementService).setEnablement('pub.c', true); + instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [aLocalExtension('a', { extensionDependencies: ['pub.b'] }), aLocalExtension('b', { extensionDependencies: ['pub.a'] }), aLocalExtension('c')]); + instantiationService.stubPromise(IChoiceService, 'choose', 1); + + testObject = instantiationService.createInstance(ExtensionsWorkbenchService); + + testObject.setEnablement(testObject.local[0], false); + + assert.ok(testObject.local[0].disabledGlobally); + assert.ok(testObject.local[1].disabledGlobally); + }); + + test('test disable extension if its dependency is dependent and is disabled', () => { + instantiationService.get(IExtensionEnablementService).setEnablement('pub.a', true); + instantiationService.get(IExtensionEnablementService).setEnablement('pub.b', false); + instantiationService.get(IExtensionEnablementService).setEnablement('pub.c', true); + instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [aLocalExtension('a', { extensionDependencies: ['pub.b'] }), aLocalExtension('b'), aLocalExtension('c', { extensionDependencies: ['pub.b'] })]); + instantiationService.stubPromise(IChoiceService, 'choose', 1); + + testObject = instantiationService.createInstance(ExtensionsWorkbenchService); + + testObject.setEnablement(testObject.local[0], false); + + assert.ok(testObject.local[0].disabledGlobally); + }); + + test('test disable extension with cyclic dependencies', () => { + instantiationService.get(IExtensionEnablementService).setEnablement('pub.a', true); + instantiationService.get(IExtensionEnablementService).setEnablement('pub.b', true); + instantiationService.get(IExtensionEnablementService).setEnablement('pub.c', true); + instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [aLocalExtension('a', { extensionDependencies: ['pub.b'] }), aLocalExtension('b', { extensionDependencies: ['pub.c'] }), aLocalExtension('c', { extensionDependencies: ['pub.a'] })]); + instantiationService.stubPromise(IChoiceService, 'choose', 1); + + testObject = instantiationService.createInstance(ExtensionsWorkbenchService); + + testObject.setEnablement(testObject.local[0], false); + + assert.ok(testObject.local[0].disabledGlobally); + assert.ok(testObject.local[1].disabledGlobally); + assert.ok(testObject.local[2].disabledGlobally); + }); + + test('test enable extension with dependencies enable all', () => { + instantiationService.get(IExtensionEnablementService).setEnablement('pub.a', false); + instantiationService.get(IExtensionEnablementService).setEnablement('pub.b', false); + instantiationService.get(IExtensionEnablementService).setEnablement('pub.c', false); + instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [aLocalExtension('a', { extensionDependencies: ['pub.b'] }), aLocalExtension('b'), aLocalExtension('c')]); + testObject = instantiationService.createInstance(ExtensionsWorkbenchService); + + testObject.setEnablement(testObject.local[0], true); + + assert.ok(!testObject.local[0].disabledGlobally); + assert.ok(!testObject.local[1].disabledGlobally); + }); + + test('test enable extension with cyclic dependencies', () => { + instantiationService.get(IExtensionEnablementService).setEnablement('pub.a', false); + instantiationService.get(IExtensionEnablementService).setEnablement('pub.b', false); + instantiationService.get(IExtensionEnablementService).setEnablement('pub.c', false); + instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [aLocalExtension('a', { extensionDependencies: ['pub.b'] }), aLocalExtension('b', { extensionDependencies: ['pub.c'] }), aLocalExtension('c', { extensionDependencies: ['pub.a'] })]); + + testObject = instantiationService.createInstance(ExtensionsWorkbenchService); + + testObject.setEnablement(testObject.local[0], true); + + assert.ok(!testObject.local[0].disabledGlobally); + assert.ok(!testObject.local[1].disabledGlobally); + assert.ok(!testObject.local[2].disabledGlobally); + }); + test('test change event is fired when disablement flags are changed', () => { instantiationService.get(IExtensionEnablementService).setEnablement('pub.c', false); instantiationService.get(IExtensionEnablementService).setEnablement('pub.b', false, true); @@ -814,7 +968,7 @@ suite('ExtensionsWorkbenchService Test', () => { function aLocalExtension(name: string = 'someext', manifest: any = {}, properties: any = {}): ILocalExtension { const localExtension = Object.create({ manifest: {} }); - assign(localExtension, { type: LocalExtensionType.User, id: generateUuid() }, properties); + assign(localExtension, { type: LocalExtensionType.User, id: generateUuid(), manifest: {} }, properties); assign(localExtension.manifest, { name, publisher: 'pub' }, manifest); localExtension.metadata = { id: localExtension.id, publisherId: localExtension.manifest.publisher, publisherDisplayName: 'somename' }; return localExtension; diff --git a/src/vs/workbench/parts/files/browser/editors/textFileEditor.ts b/src/vs/workbench/parts/files/browser/editors/textFileEditor.ts index 38f317eaaea..954cea1054f 100644 --- a/src/vs/workbench/parts/files/browser/editors/textFileEditor.ts +++ b/src/vs/workbench/parts/files/browser/editors/textFileEditor.ts @@ -206,8 +206,8 @@ export class TextFileEditor extends BaseTextEditor { return true; // in any case we handled it } - protected getCodeEditorOptions(): IEditorOptions { - const options = super.getCodeEditorOptions(); + protected getConfigurationOverrides(): IEditorOptions { + const options = super.getConfigurationOverrides(); const input = this.input; const inputName = input && input.getName(); diff --git a/src/vs/workbench/parts/files/common/explorerViewModel.ts b/src/vs/workbench/parts/files/common/explorerViewModel.ts index 35974c06d7b..c1b6e8620f0 100644 --- a/src/vs/workbench/parts/files/common/explorerViewModel.ts +++ b/src/vs/workbench/parts/files/common/explorerViewModel.ts @@ -129,26 +129,6 @@ export class FileStat implements IFileStat { } } - /** - * Returns a deep copy of this model object. - */ - public clone(): FileStat { - const stat = new FileStat(URI.parse(this.resource.toString()), this.isDirectory, this.hasChildren, this.name, this.mtime, this.etag); - stat.isDirectoryResolved = this.isDirectoryResolved; - - if (this.parent) { - stat.parent = this.parent; - } - - if (this.isDirectory) { - this.children.forEach((child: FileStat) => { - stat.addChild(child.clone()); - }); - } - - return stat; - } - /** * Adds a child element to this folder. */ @@ -315,16 +295,6 @@ export class NewStatPlaceholder extends FileStat { return this.directoryPlaceholder; } - /** - * Returns a deep copy of this model object. - */ - public clone(): NewStatPlaceholder { - const stat = new NewStatPlaceholder(this.isDirectory); - stat.parent = this.parent; - - return stat; - } - public addChild(child: NewStatPlaceholder): void { throw new Error('Can\'t perform operations in NewStatPlaceholder.'); } diff --git a/src/vs/workbench/parts/html/browser/htmlPreviewPart.ts b/src/vs/workbench/parts/html/browser/htmlPreviewPart.ts index 9b70ec70473..a87a915d26b 100644 --- a/src/vs/workbench/parts/html/browser/htmlPreviewPart.ts +++ b/src/vs/workbench/parts/html/browser/htmlPreviewPart.ts @@ -72,7 +72,7 @@ export class HtmlPreviewPart extends BaseEditor { super.dispose(); } - public createEditor(parent: Builder): void { + protected createEditor(parent: Builder): void { this._container = document.createElement('div'); this._container.style.paddingLeft = '20px'; parent.getHTMLElement().appendChild(this._container); @@ -101,7 +101,7 @@ export class HtmlPreviewPart extends BaseEditor { super.changePosition(position); } - public setEditorVisible(visible: boolean, position?: Position): void { + protected setEditorVisible(visible: boolean, position?: Position): void { this._doSetVisible(visible); super.setEditorVisible(visible, position); } diff --git a/src/vs/workbench/parts/markers/common/messages.ts b/src/vs/workbench/parts/markers/common/messages.ts index f32eb9144b5..81d430a00e7 100644 --- a/src/vs/workbench/parts/markers/common/messages.ts +++ b/src/vs/workbench/parts/markers/common/messages.ts @@ -37,9 +37,9 @@ export default class Messages { public static MARKERS_PANEL_SINGLE_UNKNOWN_LABEL: string = nls.localize('markers.panel.single.unknown.label', "1 Unknown"); public static MARKERS_PANEL_MULTIPLE_UNKNOWNS_LABEL = (noOfUnknowns: number): string => { return nls.localize('markers.panel.multiple.unknowns.label', "{0} Unknowns", '' + noOfUnknowns); }; - public static MARKERS_PANEL_AT_LINE_COL_NUMBER = (ln: number, col: number): string => { return nls.localize('markers.panel.at.ln.col.number', "({0}, {1})", '' + ln, '' + col); } + public static MARKERS_PANEL_AT_LINE_COL_NUMBER = (ln: number, col: number): string => { return nls.localize('markers.panel.at.ln.col.number', "({0}, {1})", '' + ln, '' + col); }; - public static MARKERS_TREE_ARIA_LABEL_RESOURCE = (fileName, noOfProblems): string => { return nls.localize('problems.tree.aria.label.resource', "{0} with {1} problems", fileName, noOfProblems); } + public static MARKERS_TREE_ARIA_LABEL_RESOURCE = (fileName, noOfProblems): string => { return nls.localize('problems.tree.aria.label.resource', "{0} with {1} problems", fileName, noOfProblems); }; public static MARKERS_TREE_ARIA_LABEL_MARKER = (marker: IMarker): string => { switch (marker.severity) { case Severity.Error: diff --git a/src/vs/workbench/parts/output/browser/outputPanel.ts b/src/vs/workbench/parts/output/browser/outputPanel.ts index 3967a3dae21..9a1b48d3f51 100644 --- a/src/vs/workbench/parts/output/browser/outputPanel.ts +++ b/src/vs/workbench/parts/output/browser/outputPanel.ts @@ -75,8 +75,8 @@ export class OutputPanel extends TextResourceEditor { return super.getActionItem(action); } - protected getCodeEditorOptions(): IEditorOptions { - const options = super.getCodeEditorOptions(); + protected getConfigurationOverrides(): IEditorOptions { + const options = super.getConfigurationOverrides(); options.wrappingColumn = 0; // all output editors wrap options.lineNumbers = 'off'; // all output editors hide line numbers options.glyphMargin = false; @@ -96,7 +96,7 @@ export class OutputPanel extends TextResourceEditor { return super.setInput(input, options).then(() => this.revealLastLine()); } - public createEditor(parent: Builder): void { + protected createEditor(parent: Builder): void { // First create the scoped instantation service and only then construct the editor using the scoped service const scopedContextKeyService = this.contextKeyService.createScoped(parent.getHTMLElement()); diff --git a/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts b/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts index 96c0a7da7ba..3b1e00ae280 100644 --- a/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts +++ b/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts @@ -120,20 +120,20 @@ export class DefaultPreferencesEditor extends BaseTextEditor { this.delayedFilterLogging = new Delayer(1000); } - public createEditorControl(parent: Builder): editorCommon.IEditor { + public createEditorControl(parent: Builder, configuration: editorCommon.IEditorOptions): editorCommon.IEditor { const parentContainer = parent.getHTMLElement(); this.defaultSettingHeaderWidget = this._register(this.instantiationService.createInstance(DefaultSettingsHeaderWidget, parentContainer)); this._register(this.defaultSettingHeaderWidget.onDidChange(value => this.filterPreferences(value))); this._register(this.defaultSettingHeaderWidget.onEnter(value => this.focusNextPreference())); - const defaultPreferencesEditor = this.instantiationService.createInstance(DefaultPreferencesCodeEditor, parentContainer, this.getCodeEditorOptions()); + const defaultPreferencesEditor = this.instantiationService.createInstance(DefaultPreferencesCodeEditor, parentContainer, configuration); return defaultPreferencesEditor; } - protected getCodeEditorOptions(): editorCommon.IEditorOptions { - const options = super.getCodeEditorOptions(); + protected getConfigurationOverrides(): editorCommon.IEditorOptions { + const options = super.getConfigurationOverrides(); options.readOnly = true; if (this.input) { options.lineNumbers = 'off'; diff --git a/src/vs/workbench/parts/search/browser/searchActions.ts b/src/vs/workbench/parts/search/browser/searchActions.ts index c514ad5bab9..a1a56e31347 100644 --- a/src/vs/workbench/parts/search/browser/searchActions.ts +++ b/src/vs/workbench/parts/search/browser/searchActions.ts @@ -31,8 +31,8 @@ export function isSearchViewletFocussed(viewletService: IViewletService): boolea return activeViewlet && activeViewlet.getId() === Constants.VIEWLET_ID && activeElement && DOM.isAncestor(activeElement, (activeViewlet).getContainer().getHTMLElement()); } -export function appendKeyBindingLabel(label: string, keyBinding: Keybinding, keyBindingService2: IKeybindingService): string -export function appendKeyBindingLabel(label: string, keyBinding: number, keyBindingService2: IKeybindingService): string +export function appendKeyBindingLabel(label: string, keyBinding: Keybinding, keyBindingService2: IKeybindingService): string; +export function appendKeyBindingLabel(label: string, keyBinding: number, keyBindingService2: IKeybindingService): string; export function appendKeyBindingLabel(label: string, keyBinding: any, keyBindingService2: IKeybindingService): string { keyBinding = typeof keyBinding === 'number' ? new Keybinding(keyBinding) : keyBinding; return keyBinding ? label + ' (' + keyBindingService2.getLabelFor(keyBinding) + ')' : label; diff --git a/src/vs/workbench/parts/search/browser/searchViewlet.ts b/src/vs/workbench/parts/search/browser/searchViewlet.ts index 0b9507782e3..050e2799129 100644 --- a/src/vs/workbench/parts/search/browser/searchViewlet.ts +++ b/src/vs/workbench/parts/search/browser/searchViewlet.ts @@ -828,8 +828,8 @@ export class SearchViewlet extends Viewlet { private onQueryTriggered(query: ISearchQuery, excludePattern: string, includePattern: string): void { this.viewModel.cancelSearch(); - // Progress total is 100% - let progressTotal = 100; + // Progress total is 100.0% for more progress bar granularity + let progressTotal = 1000; let progressRunner = this.progressService.show(progressTotal); let progressWorked = 0; @@ -989,7 +989,7 @@ export class SearchViewlet extends Viewlet { // Progress bar update let fakeProgress = true; if (total > 0 && worked > 0) { - let ratio = Math.round((worked / total) * 100); + let ratio = Math.round((worked / total) * progressTotal); if (ratio > progressWorked) { // never show less progress than what we have already progressRunner.worked(ratio - progressWorked); progressWorked = ratio; @@ -997,11 +997,17 @@ export class SearchViewlet extends Viewlet { } } - // Fake progress up to 90% - if (fakeProgress && progressWorked < 90) { - progressWorked++; - progressRunner.worked(1); + // Fake progress up to 90%, or when actual progress beats it + const fakeMax = 900; + const fakeMultiplier = 15; + if (fakeProgress && progressWorked < fakeMax) { + // Linearly decrease the rate of fake progress. + // 1 is the smallest allowed amount of progress. + const fakeAmt = Math.round((fakeMax - progressWorked) / fakeMax * fakeMultiplier) || 1; + progressWorked += fakeAmt; + progressRunner.worked(fakeAmt); } + // Search result tree update let count = this.viewModel.searchResult.fileCount(); if (visibleMatches !== count) { @@ -1016,7 +1022,7 @@ export class SearchViewlet extends Viewlet { this.actionRegistry['vs.tree.collapse'].enabled = true; } } - }, 200); + }, 100); this.searchWidget.setReplaceAllActionState(false); // this.replaceService.disposeAllReplacePreviews(); diff --git a/src/vs/workbench/parts/search/browser/searchWidget.ts b/src/vs/workbench/parts/search/browser/searchWidget.ts index 8a9c4a9ef76..c2bdb2895a7 100644 --- a/src/vs/workbench/parts/search/browser/searchWidget.ts +++ b/src/vs/workbench/parts/search/browser/searchWidget.ts @@ -71,7 +71,7 @@ export class SearchWidget extends Widget { private static REPLACE_ALL_ENABLED_LABEL = (keyBindingService2: IKeybindingService): string => { let keybindings = keyBindingService2.lookupKeybindings(ReplaceAllAction.ID); return appendKeyBindingLabel(nls.localize('search.action.replaceAll.enabled.label', "Replace All"), keybindings[0], keyBindingService2); - }; + } public domNode: HTMLElement; public searchInput: FindInput; diff --git a/src/vs/workbench/parts/terminal/common/terminal.ts b/src/vs/workbench/parts/terminal/common/terminal.ts index 65f656ead29..6be089c31a7 100644 --- a/src/vs/workbench/parts/terminal/common/terminal.ts +++ b/src/vs/workbench/parts/terminal/common/terminal.ts @@ -100,7 +100,7 @@ export interface ITerminalService { onInstanceTitleChanged: Event; terminalInstances: ITerminalInstance[]; - createInstance(name?: string, shellPath?: string, shellArgs?: string[], ignoreCustomCwd?: boolean): ITerminalInstance; + createInstance(name?: string, shellPath?: string, shellArgs?: string[], waitOnExit?: boolean, ignoreCustomCwd?: boolean): ITerminalInstance; getInstanceFromId(terminalId: number): ITerminalInstance; getInstanceLabels(): string[]; getActiveInstance(): ITerminalInstance; diff --git a/src/vs/workbench/parts/terminal/electron-browser/media/terminal.css b/src/vs/workbench/parts/terminal/electron-browser/media/terminal.css index 2fa59769fa7..031c18fbda5 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/media/terminal.css +++ b/src/vs/workbench/parts/terminal/electron-browser/media/terminal.css @@ -38,10 +38,11 @@ } .monaco-workbench .panel.integrated-terminal .xterm-viewport { - /* Subtract right margin so that the scroll bar aligns with size of panel */ - margin-right: -20px; - /* Force 100% height so that the scroll bar is aligned to the top and the bottom of the panel, the terminal rows may not be top aligned */ - height: 100% !important; + /* Align the viewport to the bottom of the panel, just like the terminal */ + position: absolute; + right: 0; + bottom: 0; + left: 0; } /* Terminal actions */ diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminalInstance.ts b/src/vs/workbench/parts/terminal/electron-browser/terminalInstance.ts index 6bfcc2aecef..d97c72fc963 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/terminalInstance.ts +++ b/src/vs/workbench/parts/terminal/electron-browser/terminalInstance.ts @@ -373,12 +373,12 @@ export class TerminalInstance implements ITerminalInstance { this._isExiting = true; let exitCodeMessage: string; - if (exitCode !== 0) { + if (exitCode) { exitCodeMessage = nls.localize('terminal.integrated.exitedWithCode', 'The terminal process terminated with exit code: {0}', exitCode); } if (this._shellLaunchConfig.waitOnExit) { - if (exitCode !== 0) { + if (exitCode) { this._xterm.writeln(exitCodeMessage); } this._xterm.writeln(nls.localize('terminal.integrated.waitOnExit', 'Press any key to close the terminal')); @@ -389,7 +389,7 @@ export class TerminalInstance implements ITerminalInstance { }); } else { this.dispose(); - if (exitCode !== 0) { + if (exitCode) { if (this._isLaunching) { let args = ''; if (this._shellLaunchConfig.args && this._shellLaunchConfig.args.length) { diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminalService.ts b/src/vs/workbench/parts/terminal/electron-browser/terminalService.ts index 23bb2c568b4..da6adf7cca8 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/terminalService.ts +++ b/src/vs/workbench/parts/terminal/electron-browser/terminalService.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import Event, { Emitter } from 'vs/base/common/event'; +import * as errors from 'vs/base/common/errors'; import platform = require('vs/base/common/platform'); import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -60,10 +61,11 @@ export class TerminalService implements ITerminalService { this.onInstanceDisposed((terminalInstance) => { this._removeInstance(terminalInstance); }); } - public createInstance(name?: string, shellPath?: string, shellArgs?: string[], ignoreCustomCwd?: boolean): ITerminalInstance { + public createInstance(name?: string, shellPath?: string, shellArgs?: string[], waitOnExit?: boolean, ignoreCustomCwd?: boolean): ITerminalInstance { let shell: IShellLaunchConfig = { executable: shellPath, args: shellArgs, + waitOnExit, ignoreCustomCwd }; let terminalInstance = this._instantiationService.createInstance(TerminalInstance, @@ -189,7 +191,7 @@ export class TerminalService implements ITerminalService { public hidePanel(): void { const panel = this._panelService.getActivePanel(); if (panel && panel.getId() === TERMINAL_PANEL_ID) { - this._partService.setPanelHidden(true); + this._partService.setPanelHidden(true).done(undefined, errors.onUnexpectedError); } } diff --git a/src/vs/workbench/services/configuration/common/configuration.ts b/src/vs/workbench/services/configuration/common/configuration.ts index 5a5f277bb74..e5e5f4c53de 100644 --- a/src/vs/workbench/services/configuration/common/configuration.ts +++ b/src/vs/workbench/services/configuration/common/configuration.ts @@ -12,7 +12,7 @@ export const WORKSPACE_CONFIG_DEFAULT_PATH = `${WORKSPACE_CONFIG_FOLDER_DEFAULT_ export const IWorkspaceConfigurationService = createDecorator('configurationService'); -export type IWorkspaceConfigurationValues = { [key: string]: IWorkspaceConfigurationValue } +export type IWorkspaceConfigurationValues = { [key: string]: IWorkspaceConfigurationValue }; export interface IWorkspaceConfigurationService extends IConfigurationService { diff --git a/src/vs/workbench/services/part/common/partService.ts b/src/vs/workbench/services/part/common/partService.ts index 5036d29cf88..47cca47baed 100644 --- a/src/vs/workbench/services/part/common/partService.ts +++ b/src/vs/workbench/services/part/common/partService.ts @@ -23,7 +23,6 @@ export enum Position { } export interface ILayoutOptions { - forceStyleRecompute?: boolean; toggleMaximizedPanel?: boolean; } @@ -80,12 +79,12 @@ export interface IPartService { /** * Set sidebar hidden or not */ - setSideBarHidden(hidden: boolean): void; + setSideBarHidden(hidden: boolean): TPromise; /** * Set panel part hidden or not */ - setPanelHidden(hidden: boolean): void; + setPanelHidden(hidden: boolean): TPromise; /** * Maximizes the panel height if the panel is not already maximized. diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts b/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts index 6ea355c39cc..22dc6e92a6b 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts @@ -321,9 +321,19 @@ export class TextFileEditorModelManager implements ITextFileEditorModelManager { } public getAll(resource?: URI): ITextFileEditorModel[] { - return Object.keys(this.mapResourceToModel) - .filter(r => !resource || resource.toString() === r) - .map(r => this.mapResourceToModel[r]); + if (resource) { + const res = this.mapResourceToModel[resource.toString()]; + + return res ? [res] : []; + } + + const keys = Object.keys(this.mapResourceToModel); + const res: ITextFileEditorModel[] = []; + for (let i = 0; i < keys.length; i++) { + res.push(this.mapResourceToModel[keys[i]]); + } + + return res; } public add(resource: URI, model: ITextFileEditorModel): void { diff --git a/src/vs/workbench/test/browser/part.test.ts b/src/vs/workbench/test/browser/part.test.ts index 06d23ce51f7..ffd2aeb186e 100644 --- a/src/vs/workbench/test/browser/part.test.ts +++ b/src/vs/workbench/test/browser/part.test.ts @@ -17,7 +17,7 @@ import { TestWorkspace } from 'vs/platform/workspace/test/common/testWorkspace'; class MyPart extends Part { constructor(private expectedParent: Builder) { - super('myPart'); + super('myPart', { hasTitle: true }); } public createTitleArea(parent: Builder): Builder { @@ -30,11 +30,6 @@ class MyPart extends Part { return super.createContentArea(parent); } - public createStatusArea(parent: Builder): Builder { - assert.strictEqual(parent, this.expectedParent); - return super.createStatusArea(parent); - } - public getMemento(storageService: IStorageService): any { return super.getMemento(storageService); } @@ -43,7 +38,7 @@ class MyPart extends Part { class MyPart2 extends Part { constructor() { - super('myPart2'); + super('myPart2', { hasTitle: true }); } public createTitleArea(parent: Builder): Builder { @@ -63,21 +58,12 @@ class MyPart2 extends Part { }); }); } - - public createStatusArea(parent: Builder): Builder { - return parent.div(function (div) { - div.span({ - id: 'myPart.status', - innerHtml: 'Status' - }); - }); - } } class MyPart3 extends Part { constructor() { - super('myPart2'); + super('myPart2', { hasTitle: false }); } public createTitleArea(parent: Builder): Builder { @@ -92,10 +78,6 @@ class MyPart3 extends Part { }); }); } - - public createStatusArea(parent: Builder): Builder { - return null; - } } suite('Workbench Part', () => { @@ -153,7 +135,7 @@ suite('Workbench Part', () => { assert.strictEqual(Types.isEmptyObject(memento), true); }); - test('Part Layout with Title, Content and Status', function () { + test('Part Layout with Title and Content', function () { let b = Build.withElementById(fixtureId); b.div().hide(); @@ -162,7 +144,6 @@ suite('Workbench Part', () => { assert(Build.withElementById('myPart.title')); assert(Build.withElementById('myPart.content')); - assert(Build.withElementById('myPart.status')); }); test('Part Layout with Content only', function () { @@ -174,6 +155,5 @@ suite('Workbench Part', () => { assert(!Build.withElementById('myPart.title')); assert(Build.withElementById('myPart.content')); - assert(!Build.withElementById('myPart.status')); }); }); \ No newline at end of file diff --git a/src/vs/workbench/test/common/editor/rangeDecorations.test.ts b/src/vs/workbench/test/common/editor/rangeDecorations.test.ts index d7c86209e15..bde6f79405e 100644 --- a/src/vs/workbench/test/common/editor/rangeDecorations.test.ts +++ b/src/vs/workbench/test/common/editor/rangeDecorations.test.ts @@ -150,7 +150,7 @@ suite('Editor - Range decorations', () => { return model; } - function mockEditorService(editorInput: IEditorInput) + function mockEditorService(editorInput: IEditorInput); function mockEditorService(resource: URI) function mockEditorService(arg: any) { let editorInput: IEditorInput = arg instanceof URI ? instantiationService.createInstance(FileEditorInput, arg, void 0) : arg; diff --git a/src/vs/workbench/test/workbenchTestServices.ts b/src/vs/workbench/test/workbenchTestServices.ts index 22652887145..a336de3b3e7 100644 --- a/src/vs/workbench/test/workbenchTestServices.ts +++ b/src/vs/workbench/test/workbenchTestServices.ts @@ -272,13 +272,13 @@ export class TestPartService implements IPartService { return false; } - public setSideBarHidden(hidden: boolean): void { } + public setSideBarHidden(hidden: boolean): TPromise { return TPromise.as(null); } public isPanelHidden(): boolean { return false; } - public setPanelHidden(hidden: boolean): void { } + public setPanelHidden(hidden: boolean): TPromise { return TPromise.as(null); } public toggleMaximizedPanel(): void { } diff --git a/tslint.json b/tslint.json index 48f668ec5a6..084f4addff2 100644 --- a/tslint.json +++ b/tslint.json @@ -2,7 +2,6 @@ "rules": { "no-unused-expression": true, "no-duplicate-variable": true, - "no-duplicate-key": true, "no-unused-variable": true, "curly": true, "class-name": true,